function renderSoundDesignSection(script) { soundGrid.innerHTML = ''; const soundKeywords = ['explosion', 'door', 'footsteps', 'wind', 'rain', 'laser', 'engine', 'hum', 'creak', 'blast', 'rumble']; script.scenes.forEach(scene => { let soundCues = new Set(); const sceneText = scene.setting + ' ' + (scene.dialogue || []).map(d => d.line).join(' '); soundKeywords.forEach(keyword => { if (new RegExp(`\\b${keyword}s?\\b`, 'i').test(sceneText)) soundCues.add(keyword); }); let cuesHtml = 'No specific sound cues detected.'; if (soundCues.size > 0) { cuesHtml = [...soundCues].map(cue => { const cueKey = `sfx-${scene.scene_number}-${cue}`; const isApproved = !!approvedSounds[cueKey]; return `
${cue}
`; }).join(''); } const ambKey = `ambience-${scene.scene_number}`; const isAmbApproved = !!approvedSounds[ambKey]; const card = document.createElement('div'); card.className = 'bg-gray-800 rounded-2xl shadow-2xl p-6'; card.innerHTML = `

Scene ${scene.scene_number}: ${scene.setting}

Ambient Sound

Background Ambience

Sound Effects

${cuesHtml}

Music / Score

`; 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 += ``; voiceOptions += categorizedVoices[gender].map(v => ``).join(''); 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; } }
0