|
|
|
@ -1,6 +1,64 @@
|
|
|
|
|
import { getSpeakers, addSpeaker, deleteSpeaker, generateDialog } from './api.js';
|
|
|
|
|
import { API_BASE_URL, API_BASE_URL_FOR_FILES } from './config.js';
|
|
|
|
|
|
|
|
|
|
// --- Global Inline Notification Helpers --- //
|
|
|
|
|
const noticeEl = document.getElementById('global-notice');
|
|
|
|
|
const noticeContentEl = document.getElementById('global-notice-content');
|
|
|
|
|
const noticeActionsEl = document.getElementById('global-notice-actions');
|
|
|
|
|
const noticeCloseBtn = document.getElementById('global-notice-close');
|
|
|
|
|
|
|
|
|
|
function hideNotice() {
|
|
|
|
|
if (!noticeEl) return;
|
|
|
|
|
noticeEl.style.display = 'none';
|
|
|
|
|
noticeEl.className = 'notice';
|
|
|
|
|
if (noticeContentEl) noticeContentEl.textContent = '';
|
|
|
|
|
if (noticeActionsEl) noticeActionsEl.innerHTML = '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showNotice(message, type = 'info', options = {}) {
|
|
|
|
|
if (!noticeEl || !noticeContentEl || !noticeActionsEl) {
|
|
|
|
|
console[type === 'error' ? 'error' : 'log']('[NOTICE]', message);
|
|
|
|
|
return () => {};
|
|
|
|
|
}
|
|
|
|
|
const { timeout = null, actions = [] } = options;
|
|
|
|
|
noticeEl.className = `notice notice--${type}`;
|
|
|
|
|
noticeContentEl.textContent = message;
|
|
|
|
|
noticeActionsEl.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
actions.forEach(({ text, primary = false, onClick }) => {
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.textContent = text;
|
|
|
|
|
if (primary) btn.classList.add('btn-primary');
|
|
|
|
|
btn.onclick = () => {
|
|
|
|
|
try { onClick && onClick(); } finally { hideNotice(); }
|
|
|
|
|
};
|
|
|
|
|
noticeActionsEl.appendChild(btn);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (noticeCloseBtn) noticeCloseBtn.onclick = hideNotice;
|
|
|
|
|
noticeEl.style.display = 'flex';
|
|
|
|
|
|
|
|
|
|
let timerId = null;
|
|
|
|
|
if (timeout && Number.isFinite(timeout)) {
|
|
|
|
|
timerId = window.setTimeout(hideNotice, timeout);
|
|
|
|
|
}
|
|
|
|
|
return () => {
|
|
|
|
|
if (timerId) window.clearTimeout(timerId);
|
|
|
|
|
hideNotice();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function confirmAction(message) {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
showNotice(message, 'warning', {
|
|
|
|
|
actions: [
|
|
|
|
|
{ text: 'Cancel', primary: false, onClick: () => resolve(false) },
|
|
|
|
|
{ text: 'Confirm', primary: true, onClick: () => resolve(true) },
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
|
|
console.log('DOM fully loaded and parsed');
|
|
|
|
|
initializeSpeakerManagement();
|
|
|
|
@ -23,18 +81,24 @@ function initializeSpeakerManagement() {
|
|
|
|
|
const audioFile = formData.get('audio_file');
|
|
|
|
|
|
|
|
|
|
if (!speakerName || !audioFile || audioFile.size === 0) {
|
|
|
|
|
alert('Please provide a speaker name and an audio file.');
|
|
|
|
|
showNotice('Please provide a speaker name and an audio file.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const submitBtn = addSpeakerForm.querySelector('button[type="submit"]');
|
|
|
|
|
const prevText = submitBtn ? submitBtn.textContent : null;
|
|
|
|
|
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Adding…'; }
|
|
|
|
|
const newSpeaker = await addSpeaker(formData);
|
|
|
|
|
alert(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`);
|
|
|
|
|
showNotice(`Speaker added: ${newSpeaker.name} (ID: ${newSpeaker.id})`, 'success', { timeout: 3000 });
|
|
|
|
|
addSpeakerForm.reset();
|
|
|
|
|
loadSpeakers(); // Refresh speaker list
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to add speaker:', error);
|
|
|
|
|
alert('Error adding speaker: ' + error.message);
|
|
|
|
|
showNotice('Error adding speaker: ' + error.message, 'error');
|
|
|
|
|
} finally {
|
|
|
|
|
const submitBtn = addSpeakerForm.querySelector('button[type="submit"]');
|
|
|
|
|
if (submitBtn) { submitBtn.disabled = false; submitBtn.textContent = 'Add Speaker'; }
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -79,23 +143,24 @@ async function loadSpeakers() {
|
|
|
|
|
} 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);
|
|
|
|
|
showNotice('Error loading speakers: ' + error.message, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleDeleteSpeaker(speakerId) {
|
|
|
|
|
if (!speakerId) {
|
|
|
|
|
alert('Cannot delete speaker: Speaker ID is missing.');
|
|
|
|
|
showNotice('Cannot delete speaker: Speaker ID is missing.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!confirm(`Are you sure you want to delete speaker ${speakerId}?`)) return;
|
|
|
|
|
const ok = await confirmAction(`Are you sure you want to delete speaker ${speakerId}?`);
|
|
|
|
|
if (!ok) return;
|
|
|
|
|
try {
|
|
|
|
|
await deleteSpeaker(speakerId);
|
|
|
|
|
alert(`Speaker ${speakerId} deleted successfully.`);
|
|
|
|
|
showNotice(`Speaker ${speakerId} deleted successfully.`, 'success', { timeout: 3000 });
|
|
|
|
|
loadSpeakers(); // Refresh speaker list
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(`Failed to delete speaker ${speakerId}:`, error);
|
|
|
|
|
alert(`Error deleting speaker: ${error.message}`);
|
|
|
|
|
showNotice(`Error deleting speaker: ${error.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -318,7 +383,7 @@ async function initializeDialogEditor() {
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error in generateLine:', err);
|
|
|
|
|
dialogItems[index].error = err.message || 'Failed to generate audio.';
|
|
|
|
|
alert(dialogItems[index].error);
|
|
|
|
|
showNotice(dialogItems[index].error, 'error');
|
|
|
|
|
} finally {
|
|
|
|
|
dialogItems[index].isGenerating = false;
|
|
|
|
|
renderDialogItems();
|
|
|
|
@ -380,13 +445,13 @@ async function initializeDialogEditor() {
|
|
|
|
|
try {
|
|
|
|
|
availableSpeakersCache = await getSpeakers();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
alert('Could not load speakers. Please try again.');
|
|
|
|
|
showNotice('Could not load speakers. Please try again.', 'error');
|
|
|
|
|
console.error('Error fetching speakers for dialog:', error);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (availableSpeakersCache.length === 0) {
|
|
|
|
|
alert('No speakers available. Please add a speaker first.');
|
|
|
|
|
showNotice('No speakers available. Please add a speaker first.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -416,7 +481,7 @@ async function initializeDialogEditor() {
|
|
|
|
|
const speakerId = speakerSelect.value;
|
|
|
|
|
const text = textInput.value.trim();
|
|
|
|
|
if (!speakerId || !text) {
|
|
|
|
|
alert('Please select a speaker and enter text.');
|
|
|
|
|
showNotice('Please select a speaker and enter text.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
dialogItems.push(normalizeDialogItem({ type: 'speech', speaker_id: speakerId, text: text }));
|
|
|
|
@ -458,7 +523,7 @@ async function initializeDialogEditor() {
|
|
|
|
|
addButton.onclick = () => {
|
|
|
|
|
const duration = parseFloat(durationInput.value);
|
|
|
|
|
if (isNaN(duration) || duration <= 0) {
|
|
|
|
|
alert('Invalid duration. Please enter a positive number.');
|
|
|
|
|
showNotice('Invalid duration. Please enter a positive number.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
dialogItems.push(normalizeDialogItem({ type: 'silence', duration: duration }));
|
|
|
|
@ -483,15 +548,18 @@ async function initializeDialogEditor() {
|
|
|
|
|
generateDialogBtn.addEventListener('click', async () => {
|
|
|
|
|
const outputBaseName = outputBaseNameInput.value.trim();
|
|
|
|
|
if (!outputBaseName) {
|
|
|
|
|
alert('Please enter an output base name.');
|
|
|
|
|
showNotice('Please enter an output base name.', 'warning', { timeout: 4000 });
|
|
|
|
|
outputBaseNameInput.focus();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (dialogItems.length === 0) {
|
|
|
|
|
alert('Please add at least one speech or silence line to the dialog.');
|
|
|
|
|
showNotice('Please add at least one speech or silence line to the dialog.', 'warning', { timeout: 4000 });
|
|
|
|
|
return; // Prevent further execution if no dialog items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const prevText = generateDialogBtn.textContent;
|
|
|
|
|
generateDialogBtn.disabled = true;
|
|
|
|
|
generateDialogBtn.textContent = 'Generating…';
|
|
|
|
|
// Smart dialog-wide generation: use pre-generated audio where present
|
|
|
|
|
const dialogItemsToGenerate = dialogItems.map(item => {
|
|
|
|
|
// Only send minimal fields for items that need generation
|
|
|
|
@ -543,7 +611,11 @@ async function initializeDialogEditor() {
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Dialog generation failed:', error);
|
|
|
|
|
if (generationLogPre) generationLogPre.textContent = `Error generating dialog: ${error.message}`;
|
|
|
|
|
alert(`Error generating dialog: ${error.message}`);
|
|
|
|
|
showNotice(`Error generating dialog: ${error.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
generateDialogBtn.disabled = false;
|
|
|
|
|
generateDialogBtn.textContent = prevText;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
@ -551,7 +623,7 @@ async function initializeDialogEditor() {
|
|
|
|
|
// --- Save/Load Script Functionality ---
|
|
|
|
|
function saveDialogScript() {
|
|
|
|
|
if (dialogItems.length === 0) {
|
|
|
|
|
alert('No dialog items to save. Please add some speech or silence lines first.');
|
|
|
|
|
showNotice('No dialog items to save. Please add some speech or silence lines first.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -596,11 +668,12 @@ async function initializeDialogEditor() {
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
|
|
|
|
console.log(`Dialog script saved as ${filename}`);
|
|
|
|
|
showNotice(`Dialog script saved as ${filename}`, 'success', { timeout: 3000 });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function loadDialogScript(file) {
|
|
|
|
|
if (!file) {
|
|
|
|
|
alert('Please select a file to load.');
|
|
|
|
|
showNotice('Please select a file to load.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -623,19 +696,19 @@ async function initializeDialogEditor() {
|
|
|
|
|
}
|
|
|
|
|
} catch (parseError) {
|
|
|
|
|
console.error(`Error parsing line ${i + 1}:`, parseError);
|
|
|
|
|
alert(`Error parsing line ${i + 1}: ${parseError.message}`);
|
|
|
|
|
showNotice(`Error parsing line ${i + 1}: ${parseError.message}`, 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loadedItems.length === 0) {
|
|
|
|
|
alert('No valid dialog items found in the file.');
|
|
|
|
|
showNotice('No valid dialog items found in the file.', 'warning', { timeout: 4000 });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Confirm replacement if existing items
|
|
|
|
|
if (dialogItems.length > 0) {
|
|
|
|
|
const confirmed = confirm(
|
|
|
|
|
const confirmed = await confirmAction(
|
|
|
|
|
`This will replace your current dialog (${dialogItems.length} items) with the loaded script (${loadedItems.length} items). Continue?`
|
|
|
|
|
);
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
@ -647,7 +720,7 @@ async function initializeDialogEditor() {
|
|
|
|
|
availableSpeakersCache = await getSpeakers();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching speakers:', error);
|
|
|
|
|
alert('Could not load speakers. Dialog loaded but speaker names may not display correctly.');
|
|
|
|
|
showNotice('Could not load speakers. Dialog loaded but speaker names may not display correctly.', 'warning', { timeout: 5000 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -656,16 +729,16 @@ async function initializeDialogEditor() {
|
|
|
|
|
renderDialogItems();
|
|
|
|
|
|
|
|
|
|
console.log(`Loaded ${loadedItems.length} dialog items from script`);
|
|
|
|
|
alert(`Successfully loaded ${loadedItems.length} dialog items.`);
|
|
|
|
|
showNotice(`Successfully loaded ${loadedItems.length} dialog items.`, 'success', { timeout: 3000 });
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading dialog script:', error);
|
|
|
|
|
alert(`Error loading dialog script: ${error.message}`);
|
|
|
|
|
showNotice(`Error loading dialog script: ${error.message}`, 'error');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reader.onerror = function() {
|
|
|
|
|
alert('Error reading file. Please try again.');
|
|
|
|
|
showNotice('Error reading file. Please try again.', 'error');
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|