import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js'; const API_BASE_URL = 'http://localhost:8000'; // Assuming backend runs here // This should match the base URL from which FastAPI serves static files // If your main app is at http://localhost:8000, and static files are served from /generated_audio relative to that, // then this should be http://localhost:8000. The backend will return paths like /generated_audio/... const API_BASE_URL_FOR_FILES = 'http://localhost:8000'; document.addEventListener('DOMContentLoaded', () => { console.log('DOM fully loaded and parsed'); initializeSpeakerManagement(); initializeDialogEditor(); // Placeholder for now initializeResultsDisplay(); // Placeholder for now }); // --- Speaker Management --- // const speakerListUL = document.getElementById('speaker-list'); const addSpeakerForm = document.getElementById('add-speaker-form'); function initializeSpeakerManagement() { loadSpeakers(); if (addSpeakerForm) { addSpeakerForm.addEventListener('submit', async (event) => { event.preventDefault(); const formData = new FormData(addSpeakerForm); const speakerName = formData.get('name'); const audioFile = formData.get('audio_file'); if (!speakerName || !audioFile || audioFile.size === 0) { alert('Please provide a speaker name and an audio file.'); return; } try { const newSpeaker = await addSpeaker(formData); alert(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`); addSpeakerForm.reset(); loadSpeakers(); // Refresh speaker list } catch (error) { console.error('Failed to add speaker:', error); alert('Error adding speaker: ' + error.message); } }); } } async function loadSpeakers() { if (!speakerListUL) return; try { const speakers = await getSpeakers(); speakerListUL.innerHTML = ''; // Clear existing list if (speakers.length === 0) { const listItem = document.createElement('li'); listItem.textContent = 'No speakers available.'; speakerListUL.appendChild(listItem); return; } speakers.forEach(speaker => { const listItem = document.createElement('li'); // Create a container for the speaker name and delete button const container = document.createElement('div'); container.style.display = 'flex'; container.style.justifyContent = 'space-between'; container.style.alignItems = 'center'; container.style.width = '100%'; // Add speaker name const nameSpan = document.createElement('span'); nameSpan.textContent = speaker.name; container.appendChild(nameSpan); // Add delete button const deleteBtn = document.createElement('button'); deleteBtn.textContent = 'Delete'; deleteBtn.classList.add('delete-speaker-btn'); deleteBtn.onclick = () => handleDeleteSpeaker(speaker.id); container.appendChild(deleteBtn); listItem.appendChild(container); speakerListUL.appendChild(listItem); }); } catch (error) { console.error('Failed to load speakers:', error); speakerListUL.innerHTML = '
  • Error loading speakers. See console for details.
  • '; alert('Error loading speakers: ' + error.message); } } async function handleDeleteSpeaker(speakerId) { if (!speakerId) { alert('Cannot delete speaker: Speaker ID is missing.'); return; } if (!confirm(`Are you sure you want to delete speaker ${speakerId}?`)) return; try { await deleteSpeaker(speakerId); alert(`Speaker ${speakerId} deleted successfully.`); loadSpeakers(); // Refresh speaker list } catch (error) { console.error(`Failed to delete speaker ${speakerId}:`, error); alert(`Error deleting speaker: ${error.message}`); } } // --- Dialog Editor --- // let dialogItems = []; // Holds the sequence of speech/silence items let availableSpeakersCache = []; // To populate speaker dropdown function initializeDialogEditor() { const dialogItemsContainer = document.getElementById('dialog-items-container'); const addSpeechLineBtn = document.getElementById('add-speech-line-btn'); const addSilenceLineBtn = document.getElementById('add-silence-line-btn'); const outputBaseNameInput = document.getElementById('output-base-name'); const generateDialogBtn = document.getElementById('generate-dialog-btn'); // Results Display Elements const generationLogPre = document.getElementById('generation-log-content'); // Corrected ID const audioPlayer = document.getElementById('concatenated-audio-player'); // Corrected ID // audioSource will be the audioPlayer itself, no separate element by default in the HTML const downloadZipLink = document.getElementById('zip-archive-link'); // Corrected ID const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder'); const resultsDisplaySection = document.getElementById('results-display'); let dialogItems = []; let availableSpeakersCache = []; // Cache for speaker names and IDs // Function to render the current dialogItems array to the DOM as table rows function renderDialogItems() { if (!dialogItemsContainer) return; dialogItemsContainer.innerHTML = ''; dialogItems.forEach((item, index) => { const tr = document.createElement('tr'); // Type column const typeTd = document.createElement('td'); typeTd.textContent = item.type === 'speech' ? 'Speech' : 'Silence'; tr.appendChild(typeTd); // Speaker column const speakerTd = document.createElement('td'); if (item.type === 'speech') { const speaker = availableSpeakersCache.find(s => s.id === item.speaker_id); speakerTd.textContent = speaker ? speaker.name : 'Unknown Speaker'; } else { speakerTd.textContent = '—'; } tr.appendChild(speakerTd); // Text/Duration column const textTd = document.createElement('td'); if (item.type === 'speech') { let txt = item.text.length > 60 ? item.text.substring(0, 57) + '…' : item.text; textTd.textContent = `"${txt}"`; } else { textTd.textContent = `${item.duration}s`; } tr.appendChild(textTd); // Actions column const actionsTd = document.createElement('td'); actionsTd.classList.add('actions'); const removeBtn = document.createElement('button'); removeBtn.innerHTML = '×'; // Unicode multiplication sign (X) removeBtn.classList.add('remove-dialog-item-btn', 'x-remove-btn'); removeBtn.setAttribute('aria-label', 'Remove dialog line'); removeBtn.title = 'Remove'; removeBtn.onclick = () => { dialogItems.splice(index, 1); renderDialogItems(); }; actionsTd.appendChild(removeBtn); tr.appendChild(actionsTd); dialogItemsContainer.appendChild(tr); }); } const tempInputArea = document.getElementById('temp-input-area'); function clearTempInputArea() { if (tempInputArea) tempInputArea.innerHTML = ''; } if (addSpeechLineBtn) { addSpeechLineBtn.addEventListener('click', async () => { clearTempInputArea(); // Clear any previous inputs if (availableSpeakersCache.length === 0) { try { availableSpeakersCache = await getSpeakers(); } catch (error) { alert('Could not load speakers. Please try again.'); console.error('Error fetching speakers for dialog:', error); return; } } if (availableSpeakersCache.length === 0) { alert('No speakers available. Please add a speaker first.'); return; } const speakerSelectLabel = document.createElement('label'); speakerSelectLabel.textContent = 'Speaker: '; speakerSelectLabel.htmlFor = 'temp-speaker-select'; const speakerSelect = document.createElement('select'); speakerSelect.id = 'temp-speaker-select'; availableSpeakersCache.forEach(speaker => { const option = document.createElement('option'); option.value = speaker.id; option.textContent = speaker.name; speakerSelect.appendChild(option); }); const textInputLabel = document.createElement('label'); textInputLabel.textContent = ' Text: '; textInputLabel.htmlFor = 'temp-speech-text'; const textInput = document.createElement('textarea'); textInput.id = 'temp-speech-text'; textInput.rows = 2; textInput.placeholder = 'Enter speech text'; const addButton = document.createElement('button'); addButton.textContent = 'Add Speech'; addButton.onclick = () => { const speakerId = speakerSelect.value; const text = textInput.value.trim(); if (!speakerId || !text) { alert('Please select a speaker and enter text.'); return; } dialogItems.push({ type: 'speech', speaker_id: speakerId, text: text }); renderDialogItems(); clearTempInputArea(); }; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.onclick = clearTempInputArea; if (tempInputArea) { tempInputArea.appendChild(speakerSelectLabel); tempInputArea.appendChild(speakerSelect); tempInputArea.appendChild(textInputLabel); tempInputArea.appendChild(textInput); tempInputArea.appendChild(addButton); tempInputArea.appendChild(cancelButton); } }); } if (addSilenceLineBtn) { addSilenceLineBtn.addEventListener('click', () => { clearTempInputArea(); // Clear any previous inputs const durationInputLabel = document.createElement('label'); durationInputLabel.textContent = 'Duration (s): '; durationInputLabel.htmlFor = 'temp-silence-duration'; const durationInput = document.createElement('input'); durationInput.type = 'number'; durationInput.id = 'temp-silence-duration'; durationInput.step = '0.1'; durationInput.min = '0.1'; durationInput.placeholder = 'e.g., 0.5'; const addButton = document.createElement('button'); addButton.textContent = 'Add Silence'; addButton.onclick = () => { const duration = parseFloat(durationInput.value); if (isNaN(duration) || duration <= 0) { alert('Invalid duration. Please enter a positive number.'); return; } dialogItems.push({ type: 'silence', duration: duration }); renderDialogItems(); clearTempInputArea(); }; const cancelButton = document.createElement('button'); cancelButton.textContent = 'Cancel'; cancelButton.onclick = clearTempInputArea; if (tempInputArea) { tempInputArea.appendChild(durationInputLabel); tempInputArea.appendChild(durationInput); tempInputArea.appendChild(addButton); tempInputArea.appendChild(cancelButton); } }); } if (generateDialogBtn && outputBaseNameInput) { generateDialogBtn.addEventListener('click', async () => { const outputBaseName = outputBaseNameInput.value.trim(); if (!outputBaseName) { alert('Please enter an output base name.'); outputBaseNameInput.focus(); return; } if (dialogItems.length === 0) { alert('Please add at least one speech or silence line to the dialog.'); return; } // Clear previous results and show loading/status if (generationLogPre) generationLogPre.textContent = 'Generating dialog...'; if (audioPlayer) { audioPlayer.style.display = 'none'; audioPlayer.src = ''; // Clear previous audio source } if (downloadZipLink) { downloadZipLink.style.display = 'none'; downloadZipLink.href = '#'; downloadZipLink.textContent = ''; } if (zipArchivePlaceholder) zipArchivePlaceholder.style.display = 'block'; // Show placeholder if (resultsDisplaySection) resultsDisplaySection.style.display = 'block'; // Make sure it's visible const payload = { output_base_name: outputBaseName, dialog_items: dialogItems.map(item => { // For now, we are not collecting TTS params in the UI for speech items. // The backend will use defaults. If we add UI for these later, they'd be included here. if (item.type === 'speech') { return { type: item.type, speaker_id: item.speaker_id, text: item.text, // exaggeration: item.exaggeration, // Example for future UI enhancement // cfg_weight: item.cfg_weight, // temperature: item.temperature }; } return item; // for silence items }) }; try { console.log('Generating dialog with payload:', JSON.stringify(payload, null, 2)); const result = await generateDialog(payload); console.log('Dialog generation successful:', result); if (generationLogPre) generationLogPre.textContent = result.log || 'No log output.'; if (result.concatenated_audio_url && audioPlayer) { // Check audioPlayer, not audioSource audioPlayer.src = result.concatenated_audio_url.startsWith('http') ? result.concatenated_audio_url : `${API_BASE_URL_FOR_FILES}${result.concatenated_audio_url}`; audioPlayer.load(); // Call load() after setting new source audioPlayer.style.display = 'block'; } else { if (audioPlayer) audioPlayer.style.display = 'none'; // Ensure it's hidden if no URL if (generationLogPre) generationLogPre.textContent += '\nNo concatenated audio URL found.'; } if (result.zip_archive_url && downloadZipLink) { downloadZipLink.href = result.zip_archive_url.startsWith('http') ? result.zip_archive_url : `${API_BASE_URL_FOR_FILES}${result.zip_archive_url}`; downloadZipLink.textContent = `Download ${outputBaseName}.zip`; downloadZipLink.style.display = 'block'; if (zipArchivePlaceholder) zipArchivePlaceholder.style.display = 'none'; // Hide placeholder } else { if (downloadZipLink) downloadZipLink.style.display = 'none'; if (zipArchivePlaceholder) zipArchivePlaceholder.style.display = 'block'; // Show placeholder if no link if (generationLogPre) generationLogPre.textContent += '\nNo ZIP archive URL found.'; } } catch (error) { console.error('Dialog generation failed:', error); if (generationLogPre) generationLogPre.textContent = `Error generating dialog: ${error.message}`; alert(`Error generating dialog: ${error.message}`); } }); } console.log('Dialog Editor Initialized'); renderDialogItems(); // Initial render (empty) } // --- Results Display --- // function initializeResultsDisplay() { const generationLogContent = document.getElementById('generation-log-content'); const concatenatedAudioPlayer = document.getElementById('concatenated-audio-player'); const zipArchiveLink = document.getElementById('zip-archive-link'); const zipArchivePlaceholder = document.getElementById('zip-archive-placeholder'); // Functions to update these elements will be called by the generateDialog handler // e.g., updateLog(message), setAudioSource(url), setZipLink(url) console.log('Results Display Initialized'); }