391 lines
17 KiB
JavaScript
391 lines
17 KiB
JavaScript
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 = '<li>Error loading speakers. See console for details.</li>';
|
|
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');
|
|
}
|