`;
soundGrid.appendChild(card);
});
soundSection.classList.remove('hidden');
}
async function handleSuggestMusic(button) {
const sceneNumber = button.dataset.sceneNumber;
const scene = currentScript.scenes.find(s => s.scene_number == sceneNumber);
if (!scene) return;
const loader = button.querySelector('.loader');
const buttonText = button.querySelector('span');
const parent = button.parentElement;
const suggestionBox = parent.querySelector('.music-suggestion-text');
const playButton = parent.querySelector('.play-music-btn');
const findButton = parent.querySelector('.find-music-btn');
button.disabled = true;
loader.classList.remove('hidden');
buttonText.textContent = 'Thinking...';
hideError();
const dialogueSummary = (scene.dialogue || []).map(d => `${d.character}: ${d.line}`).join(' ');
const prompt = `Act as a film music supervisor. For a scene set in '${scene.setting}' with the dialogue summary "${dialogueSummary}", suggest a musical score. Describe the mood, instrumentation, and tempo in one or two sentences. Then, on a new line, suggest a *specific, real track title and artist from the YouTube Audio Library* that fits this mood. Format it as: Track: [Title] by [Artist].`;
const model = 'gemini-2.5-flash-preview-09-2025';
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${userApiKey}`;
const payload = { contents: [{ parts: [{ text: prompt }] }] };
try {
// --- v11.49: Use "Fast Lane" queue ---
const result = await enqueueApiCall(() => makeApiCallWithRetry(apiUrl, payload));
const text = result.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
const match = text.match(/Track: (.*) by (.*)/);
if (match && match[1] && match[2]) {
const title = match[1].trim().replace(/\.$/, '');
const artist = match[2].trim().replace(/\.$/, '');
suggestionBox.innerHTML = text.replace(/Track: .*/, `
Suggestion: ${title} by ${artist}`);
findButton.classList.remove('hidden');
} else {
suggestionBox.textContent = text;
}
sceneMusicSuggestions[sceneNumber] = text;
buttonText.textContent = 'Suggestion Generated ✔';
button.classList.remove('bg-purple-600', 'hover:bg-purple-700');
button.classList.add('bg-green-600');
playButton.classList.remove('hidden');
} else {
throw new Error("No music suggestion was returned.");
}
} catch (error) {
showError(`Could not generate music suggestion. ${error.message}`);
buttonText.textContent = 'Error';
} finally {
setTimeout(() => {
button.disabled = false;
if (buttonText.textContent !== 'Suggestion Generated ✔') {
buttonText.textContent = 'Suggest Music';
}
}, 2000);
loader.classList.add('hidden');
}
}
function initAudioContext() {
if (!audioCtx) {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} catch (e) {
showError("Web Audio API is not supported in this browser.");
console.error("Could not create AudioContext:", e);
}
}
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
async function playGeneratedMusic(suggestion, loop = false) {
initAudioContext();
if (!audioCtx) return;
if (window.currentMusicPlayer) {
window.currentMusicPlayer.stop();
}
const now = audioCtx.currentTime;
let scale = [261.63, 293.66, 329.63, 392.00, 440.00];
let duration = 0.25;
let noteInterval = 0.5;
let waveType = 'sine';
if (/piano/i.test(suggestion)) waveType = 'triangle';
if (/slow|melancholic/i.test(suggestion)) {
noteInterval = 0.75;
duration = 0.5;
scale = [261.63, 311.13, 349.23, 392.00, 466.16];
}
if (/fast|upbeat/i.test(suggestion)) {
noteInterval = 0.25;
duration = 0.125;
}
if (/tense/i.test(suggestion)) {
scale = [261.63, 277.18, 349.23, 369.99];
}
const notes = Array(8).fill(0).map(() => scale[Math.floor(Math.random() * scale.length)]);
let time = now;
const play = () => {
if (!audioCtx || audioCtx.state === 'closed') return;
notes.forEach(freq => {
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
osc.type = waveType;
osc.frequency.setValueAtTime(freq, time);
gainNode.gain.setValueAtTime(0, time);
gainNode.gain.linearRampToValueAtTime(0.15, time + 0.05);
gainNode.gain.exponentialRampToValueAtTime(0.001, time + duration);
osc.start(time);
osc.stop(time + duration);
time += noteInterval;
});
};
play();
if (loop) {
const loopInterval = setInterval(play, noteInterval * notes.length * 1000);
window.currentMusicPlayer = { stop: () => clearInterval(loopInterval) };
} else {
window.currentMusicPlayer = { stop: () => {} };
}
}
async function handleGenerateSound(button) {
initAudioContext();
if (!audioCtx) {
showError("Audio Context could not be initialized. Please interact with the page and try again.");
return;
}
const loader = button.querySelector('.loader');
const buttonText = button.querySelector('span');
const cueKey = button.dataset.cue;
button.disabled = true;
loader.classList.remove('hidden');
buttonText.textContent = '...';
hideError();
try {
if (generatedAudioCache[cueKey]) {
const audio = new Audio(generatedAudioCache[cueKey]);
audio.play();
button.classList.remove('bg-teal-600', 'bg-blue-600', 'hover:bg-teal-700', 'hover:bg-blue-700');
button.classList.add('bg-green-600');
buttonText.textContent = '✔';
return;
}
const textPrompt = button.dataset.prompt;
if (!textPrompt) throw new Error("Missing text prompt for sound generation.");
const model = "gemini-2.5-flash-preview-tts";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${userApiKey}`;
const payload = {
contents: [{ parts: [{ text: textPrompt }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: "Kore" }
}
}
},
model
};
// --- v11.49: Use "Fast Lane" queue ---
const result = await enqueueApiCall(() => makeApiCallWithRetry(apiUrl, payload));
const part = result?.candidates?.[0]?.content?.parts?.[0];
const audioData = part?.inlineData?.data;
const mimeType = part?.inlineData?.mimeType;
if (audioData && mimeType?.startsWith("audio/")) {
const sampleRate = parseInt(mimeType.match(/rate=(\d+)/)[1], 10);
const pcmData = base64ToArrayBuffer(audioData);
const pcm16 = new Int16Array(pcmData);
const wavBlob = pcmToWav(pcm16, 1, sampleRate);
const audioUrl = URL.createObjectURL(wavBlob);
generatedAudioCache[cueKey] = audioUrl;
approvedSounds[cueKey] = audioUrl;
const audio = new Audio(audioUrl);
audio.play();
button.classList.remove('bg-teal-600', 'bg-blue-600', 'hover:bg-teal-700', 'hover:bg-blue-700');
button.classList.add('bg-green-600');
buttonText.textContent = '✔';
const uploadBtn = document.querySelector(`.upload-sound-btn[data-cue="${cueKey}"]`);
if (uploadBtn) uploadBtn.classList.add('hidden');
} else {
console.error("TTS API Response missing audio data for sound:", result);
throw new Error("No valid audio data received from API.");
}
} catch (e) {
console.error("Sound generation/playback error:", e);
showError(`Could not play sound: ${e.message}.`);
buttonText.textContent = 'Error';
const uploadBtn = document.querySelector(`.upload-sound-btn[data-cue="${cueKey}"]`);
if (uploadBtn) uploadBtn.classList.remove('hidden');
} finally {
setTimeout(() => {
button.disabled = false;
loader.classList.add('hidden');
if (buttonText.textContent !== '✔') {
buttonText.textContent = 'Play';
if (button.classList.contains('generate-ambience-btn')) {
button.classList.add('bg-blue-600', 'hover:bg-blue-700');
} else {
button.classList.add('bg-teal-600', 'hover:bg-teal-700');
}
button.classList.remove('bg-green-600');
}
}, 1500);
}
}
function renderCastingSection(script) {
if (!cinematicDescriptions || !cinematicDescriptions.characters) return;
const allCharacters = cinematicDescriptions.characters;
if (allCharacters.length === 0) return;
castingGrid.innerHTML = '';
allCharacters.forEach((characterData) => {
const name = characterData.name;
const description = characterData.description;
const isCast = !!selectedCast[name];
let voiceOptions = '';
for (const gender in categorizedVoices) {
voiceOptions += ``;
}
const card = document.createElement('div');
card.id = `card-char-${name.replace(/\s+/g, '-')}`;
card.className = 'bg-gray-800 rounded-2xl shadow-2xl p-6';
card.innerHTML = `
${name}
"${description}"
${isCast ? `` : ''}
Voice Selection
`;
castingGrid.appendChild(card);
});
productionSection.classList.remove('hidden');
}
function handleUploadActorImage(event) {
const input = event.target;
const name = input.dataset.name;
const file = input.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const imageUrl = e.target.result;
selectedCast[name] = imageUrl;
const card = document.getElementById(`card-char-${name.replace(/\s+/g, '-')}`);
if (card) {
const imageGrid = card.querySelector('.image-grid');
const generateBtn = card.querySelector('.generate-casting-btn');
const voiceSection = card.querySelector('.voice-selection');
imageGrid.innerHTML = ``;
imageGrid.classList.remove('hidden');
generateBtn.classList.remove('bg-cyan-600', 'hover:bg-cyan-700');
generateBtn.classList.add('bg-green-600');
generateBtn.querySelector('span').textContent = 'Selected ✔';
voiceSection.classList.remove('hidden');
if (!selectedVoices[name]) {
const charData = cinematicDescriptions.characters.find(c => c.name === name);
const desc = charData ? charData.description.toLowerCase() : "";
if (/\b(female|woman|mother|grandmother|she|her|girl)\b/i.test(desc)) {
const femaleVoices = categorizedVoices.Female;
selectedVoices[name] = femaleVoices[Math.floor(Math.random() * femaleVoices.length)];
} else if (/\b(male|man|boy|father|grandfather|he|his)\b/i.test(desc)) {
const maleVoices = categorizedVoices.Male;
selectedVoices[name] = maleVoices[Math.floor(Math.random() * maleVoices.length)];
} else {
const femaleVoices = categorizedVoices.Female;
selectedVoices[name] = femaleVoices[Math.floor(Math.random() * femaleVoices.length)];
}
}
const voiceSelect = card.querySelector('.voice-select');
if (voiceSelect) {
voiceSelect.value = selectedVoices[name];
}
}
};
reader.readAsDataURL(file);
input.value = null;
}
async function handleGenerateCastingImages(button) {
const name = button.dataset.name;
const description = button.dataset.description;
const logline = button.dataset.logline;
const card = button.closest('.bg-gray-800'); const imageGrid = card.querySelector('.image-grid');
const loader = button.querySelector('.loader'); const buttonText = button.querySelector('span');
const voiceSection = card.querySelector('.voice-selection');
button.disabled = true; loader.classList.remove('hidden'); buttonText.textContent = 'Generating (1/4)...';
imageGrid.innerHTML = ''; imageGrid.classList.remove('hidden'); voiceSection.classList.add('hidden'); hideError();
// --- v12.5 FIX: Explicitly use 4.0 ---
const model = 'imagen-4.0-generate-001';
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:predict?key=${userApiKey}`;
let generatedImages = [];
try {
// --- v11.49 FIX: Use "Slow Lane" queue ---
for(let i = 0; i < 4; i++) {
buttonText.textContent = `Generating (${i + 1}/4)...`;
const prompt = `Photorealistic cinematic headshot of a character.
**Character:** ${name}, ${description}
**Family Context:** This character is part of a multi-generational family. Ensure the generated character is ethnically and visually consistent with a single family unit.
**Overall Story Mood:** ${logline}
**Instructions:** Generate a headshot that matches the character description, family context, AND the story's mood.
**Style:** High-detail, 8k, cinematic lighting.
**Rules:** NO text, NO words, NO watermarks, NO grids, NO panels, single image only.
**Variation:** ${i + 1}`;
const payload = { instances: [{ prompt }], parameters: { "sampleCount": 1 } };
const result = await enqueueImageApiCall(() => makeApiCallWithRetry(apiUrl, payload));
if (result.predictions && result.predictions.length > 0) {
const prediction = result.predictions[0];
const src = `data:image/png;base64,${prediction.bytesBase64Encoded}`;
generatedImages.push(src);
}
}
// --- End v11.49 FIX ---
generatedImages.forEach((src, index) => {
const img = document.createElement('img');
img.src = src;
img.dataset.src = src;
img.className = 'w-full h-auto object-cover rounded-lg concept-image casting-image';
img.onclick = () => {
card.querySelectorAll('.concept-image').forEach(i => i.classList.remove('selected'));
img.classList.add('selected'); selectedCast[name] = img.dataset.src;
button.classList.remove('bg-cyan-600', 'hover:bg-cyan-700');
button.classList.add('bg-green-600');
button.querySelector('span').textContent = 'Selected ✔';
voiceSection.classList.remove('hidden');
if (!selectedVoices[name]) {
const desc = description.toLowerCase();
if (/\b(female|woman|mother|grandmother|she|her|girl)\b/i.test(desc)) {
const femaleVoices = categorizedVoices.Female;
selectedVoices[name] = femaleVoices[Math.floor(Math.random() * femaleVoices.length)];
} else if (/\b(male|man|boy|father|grandfather|he|his)\b/i.test(desc)) {
const maleVoices = categorizedVoices.Male;
selectedVoices[name] = maleVoices[Math.floor(Math.random() * maleVoices.length)];
} else {
const femaleVoices = categorizedVoices.Female;
selectedVoices[name] = femaleVoices[Math.floor(Math.random() * femaleVoices.length)];
}
}
const voiceSelect = card.querySelector('.voice-select');
if (voiceSelect) {
voiceSelect.value = selectedVoices[name];
}
};
imageGrid.appendChild(img);
if (index === 0) img.click();
});
} catch (error) { showError(`Could not get images for ${name}. ${error.message}`);
} finally {
button.disabled = false; loader.classList.add('hidden');
if(!selectedCast[name]) buttonText.textContent = 'Regenerate';
}
}
async function handleTestVoice(button) {
initAudioContext();
if (!audioCtx) {
showError("Audio Context could not be initialized. Please interact with the page and try again.");
return;
}
const name = button.dataset.name;
const card = button.closest(".bg-gray-800");
const voice = card.querySelector(".voice-select").value;
const audioPlayer = card.querySelector(".voice-audio-player");
const loader = button.querySelector(".loader");
const buttonText = button.querySelector("span");
const volSlider = card.querySelector(".voice-volume-slider");
const volume = volSlider ? parseFloat(volSlider.value) : 0.8;
const now = Date.now();
if (lastTestTime.get(name) && now - lastTestTime.get(name) < 3000) {
showError("Please wait a few seconds before testing again.");
return;
}
lastTestTime.set(name, now);
button.disabled = true;
loader.classList.remove("hidden");
buttonText.textContent = "...";
hideError();
const textPrompt = `Say this in a neutral tone: "Hello, my name is ${name}."`;
const cacheKey = `test-${name}-${voice}`;
if (generatedAudioCache[cacheKey]) {
audioPlayer.src = generatedAudioCache[cacheKey];
audioPlayer.volume = volume;
audioPlayer.play();
button.disabled = false;
loader.classList.add("hidden");
buttonText.textContent = "Test Voice";
return;
}
const model = "gemini-2.5-flash-preview-tts";
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${userApiKey}`;
const payload = {
contents: [{ parts: [{ text: textPrompt }] }],
generationConfig: {
responseModalities: ["AUDIO"],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: {
voiceName: voice,
},
},
},
},
model: model
};
try {
// --- v11.49: Use "Fast Lane" queue ---
const result = await enqueueApiCall(() => makeApiCallWithRetry(apiUrl, payload));
const part = result?.candidates?.[0]?.content?.parts?.[0];
if (part?.inlineData?.data && part.inlineData.mimeType?.startsWith("audio/")) {
const sampleRate = parseInt(part.inlineData.mimeType.match(/rate=(\d+)/)?.[1] || "44100", 10);
const pcmData = base64ToArrayBuffer(part.inlineData.data);
const pcm16 = new Int16Array(pcmData);
const wavBlob = pcmToWav(pcm16, 1, sampleRate);
const audioUrl = URL.createObjectURL(wavBlob);
generatedAudioCache[cacheKey] = audioUrl;
audioPlayer.src = audioUrl;
audioPlayer.volume = volume;
audioPlayer.play();
} else if (part?.text) {
console.warn("Gemini returned text instead of audio:", part.text);
showError("The model returned text instead of audio. Try another voice or wait a few seconds.");
} else {
throw new Error("Valid audio data not found in API response.");
}
} catch (error) {
console.error(`Full error object for ${name}:`, error);
showError(`Could not generate voice for ${name}. Details: ${error.message}.`);
} finally {
button.disabled = false;
loader.classList.add("hidden");
buttonText.textContent = "Test Voice";
}
}
// --- Utility Functions ---
function base64ToArrayBuffer(base64) {
const binaryString = window.atob(base64); const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) { bytes[i] = binaryString.charCodeAt(i); }
return bytes.buffer;
}
function pcmToWav(pcmData, numChannels, sampleRate) {
const bitsPerSample = 16; const blockAlign = (numChannels * bitsPerSample) >> 3;
const byteRate = sampleRate * blockAlign; const dataSize = pcmData.length * (bitsPerSample >> 3);
const buffer = new ArrayBuffer(44 + dataSize); const view = new DataView(buffer);
function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } }
writeString(view, 0, 'RIFF'); view.setUint32(4, 36 + dataSize, true); writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt '); view.setUint32(16, 16, true); view.setUint16(20, 1, true);
view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true);
view.setUint32(28, byteRate, true); view.setUint16(32, blockAlign, true); view.setUint16(34, bitsPerSample, true);
writeString(view, 36, 'data'); view.setUint32(40, dataSize, true);
let offset = 44;
for (let i = 0; i < pcmData.length; i++, offset += 2) { view.setInt16(offset, pcmData[i], true); }
if (dataSize % 2 !== 0) { view.setUint8(offset, 0); }
return new Blob([view], { type: 'audio/wav' });
}
function setLoading(isLoading, buttonElement = generateBtn) {
buttonElement.disabled = isLoading;
const loaderElement = buttonElement.querySelector('.loader');
const iconElement = buttonElement.querySelector('svg');
const textElement = buttonElement.querySelector('span');
if (loaderElement) loaderElement.classList.toggle('hidden', !isLoading);
if (iconElement) iconElement.classList.toggle('hidden', isLoading);
if (textElement) {
if (isLoading) {
if (!buttonElement.dataset.originalText) {
buttonElement.dataset.originalText = textElement.textContent;
}
textElement.textContent = 'Generating...';
if (buttonElement === generateSecondCutBtn) textElement.textContent = 'Revising...';
} else {
textElement.textContent = buttonElement.dataset.originalText || 'Generate Script';
delete buttonElement.dataset.originalText;
}
}
}
function showError(message) { errorMessage.textContent = message; errorBox.classList.remove('hidden'); }
function hideError() { errorBox.classList.add('hidden'); }
// --- Master Event Listener using Delegation ---
mainContent.addEventListener('click', (event) => {
const target = event.target;
const generateLocationBtn = target.closest('.generate-location-btn');
if(generateLocationBtn) {
handleGenerateLocationImages(generateLocationBtn);
return;
}
const uploadLocationBtn = target.closest('.upload-location-btn');
if(uploadLocationBtn) {
const input = uploadLocationBtn.closest('.flex-col.md\\:flex-row')?.nextElementSibling;
if (input && input.classList.contains('location-image-upload')) {
input.click();
} else {
console.error("Could not find location image upload input.");
}
return;
}
const generateSfxBtn = target.closest('.generate-sfx-btn');
if (generateSfxBtn) {
handleGenerateSound(generateSfxBtn);
return;
}
const generateAmbienceBtn = target.closest('.generate-ambience-btn');
if (generateAmbienceBtn) {
handleGenerateSound(generateAmbienceBtn);
return;
}
const suggestMusicBtn = target.closest('.suggest-music-btn');
if (suggestMusicBtn) {
handleSuggestMusic(suggestMusicBtn);
return;
}
const playMusicBtn = target.closest('.play-music-btn');
if (playMusicBtn) {
const sceneNumber = playMusicBtn.dataset.sceneNumber;
const suggestion = sceneMusicSuggestions[sceneNumber];
if(suggestion) playGeneratedMusic(suggestion);
return;
}
const findMusicBtn = target.closest('.find-music-btn');
if (findMusicBtn) {
const sceneNumber = findMusicBtn.dataset.sceneNumber;
const suggestion = sceneMusicSuggestions[sceneNumber];
if (suggestion) {
const match = suggestion.match(/Track: (.*) by (.*)/);
if (match && match[1] && match[2]) {
const title = match[1].trim().replace(/\.$/, '');
const artist = match[2].trim().replace(/\.$/, '');
const query = encodeURIComponent(`${title} ${artist}`);
window.open(`https://www.youtube.com/results?search_query=${query}+youtube+audio+library`, '_blank');
}
}
return;
}
const generateCastingBtn = target.closest('.generate-casting-btn');
if(generateCastingBtn) {
handleGenerateCastingImages(generateCastingBtn);
return;
}
const uploadHeadshotBtn = target.closest('.upload-headshot-btn');
if(uploadHeadshotBtn) {
const name = uploadHeadshotBtn.closest('.bg-gray-800')?.querySelector('.generate-casting-btn')?.dataset.name;
if (name) {
const inputId = `upload-${name.replace(/\s+/g, '-')}`;
const input = document.getElementById(inputId);
if (input) {
input.click();
} else {
console.error("Could not find headshot upload input with ID:", inputId);
}
} else {
console.error("Could not determine character name for headshot upload.");
}
return;
}
const testVoiceBtn = target.closest('.test-voice-btn');
if(testVoiceBtn) {
handleTestVoice(testVoiceBtn);
return;
}
const saveScriptBtn = target.closest('#save-script-btn');
if (saveScriptBtn) {
handleSaveScript();
return;
}
const uploadSoundBtn = target.closest('.upload-sound-btn');
if(uploadSoundBtn) {
const cue = uploadSoundBtn.dataset.cue;
let inputToTrigger = null;
if (cue.startsWith('ambience-')) {
inputToTrigger = uploadSoundBtn.closest('.flex-col.gap-2')?.querySelector(`.sound-upload-input[data-cue="${cue}"]`);
} else {
inputToTrigger = uploadSoundBtn.closest('.bg-gray-800')?.querySelector('input.sound-upload-input:not([data-cue^="ambience-"])');
if (inputToTrigger) inputToTrigger.dataset.cue = cue;
}
if (inputToTrigger) {
inputToTrigger.click();
} else {
console.error("Could not find sound upload input for cue:", cue);
}
return;
}
});
mainContent.addEventListener('change', (event) => {
const target = event.target;
if (target.classList.contains('actor-image-upload')) {
handleUploadActorImage(event);
} else if (target.classList.contains('voice-select')) {
selectedVoices[target.dataset.name] = target.value;
Object.keys(generatedAudioCache).forEach(key => {
if (key.startsWith(`test-${target.dataset.name}-`)) {
delete generatedAudioCache[key];
}
});
} else if (target.classList.contains('location-image-upload')) {
handleUploadLocationImage(event);
} else if (target.classList.contains('sound-upload-input')) {
handleUploadSound(event);
}
});
mainContent.addEventListener('input', (event) => {
const target = event.target;
if (target.classList.contains('voice-volume-slider')) {
const name = target.dataset.name;
const player = document.querySelector(`.voice-audio-player[data-name="${name}"]`);
if (player) {
player.volume = parseFloat(target.value);
}
}
});
function handleUploadSound(event) {
const input = event.target;
const cue = input.dataset.cue;
const file = input.files[0];
if (!file || !cue) {
console.warn("Upload sound failed: Missing file or cue information.");
if(input) input.value = null;
return;
}
const reader = new FileReader();
reader.onload = (e) => {
const audioUrl = e.target.result;
approvedSounds[cue] = audioUrl;
generatedAudioCache[cue] = audioUrl;
const uploadBtn = document.querySelector(`.upload-sound-btn[data-cue="${cue}"]`);
const playBtn = document.querySelector(`.generate-sfx-btn[data-cue="${cue}"], .generate-ambience-btn[data-cue="${cue}"]`);
if (uploadBtn) {
uploadBtn.classList.add('hidden');
}
if (playBtn) {
playBtn.classList.remove('bg-teal-600', 'bg-blue-600', 'hover:bg-teal-700', 'hover:bg-blue-700');
playBtn.classList.add('bg-green-600');
playBtn.querySelector('span').textContent = '✔';
}
console.log(`Uploaded and approved sound for cue: ${cue}`);
};
reader.onerror = (error) => {
console.error("Error reading sound file:", error);
showError("Failed to read the uploaded sound file.");
};
reader.readAsDataURL(file);
input.value = null;
if (input && !input.dataset.cue?.startsWith('ambience-')) {
delete input.dataset.cue;
}
}