// ── Save voice ──────────────────────────────────────────────────────────── $('save-btn').addEventListener('click', async () => { const id = trimmedFileId||designedFileId||currentFileId; if (!id) { toast('No audio ready','error'); return; } const voiceId=$('voice-id-input').value.trim(); if (!voiceId) { toast('Enter a Voice ID','error'); return; } if (!validateVoiceId(voiceId)) { toast('Voice ID contains invalid characters','error'); return; } $('save-btn').disabled=true; try { const payload = {id, voice_id:voiceId, path:editingVoicePath, transcript:$('transcript-area').value}; const sendSave = endpoint => fetch(endpoint, {method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); let fallbackSave = false; let r = await sendSave(editingVoiceId ? '/api/voice-replace' : '/api/save'); if (editingVoiceId && (r.status === 404 || r.status === 405)) { fallbackSave = true; status('Update endpoint unavailable; saving as a regular voice…'); r = await sendSave('/api/save'); } if (!r.ok) { const e=await r.json(); throw new Error(e.detail); } const d = await r.json(); $('save-result').style.display=''; toast((editingVoiceId ? 'Voice updated: ' : 'Voice saved: ')+d.voice_id,'success'); if (fallbackSave && editingVoiceId && voiceId !== editingVoiceId) { const del = await fetch('/api/voice/' + encodeURIComponent(editingVoiceId), {method:'DELETE'}); if (!del.ok) status('Saved renamed voice; old library entry may need manual deletion.'); } editingVoiceId = null; editingVoicePath = null; } catch(e) { toast('Save failed: '+e.message,'error'); } finally { $('save-btn').disabled=false; } }); // ══════════════════════════════════════════════════════════════════════════ // LIBRARY — sort + render // ══════════════════════════════════════════════════════════════════════════ let _voices = []; let _pendingSelectId = null; let _sortField = 'id'; let _sortDir = 1; // 1 = asc, -1 = desc let _libraryIssueFilter = ''; let _activePlayButton = null; let _activePlayVoiceId = null; let _activePlayUrl = null; let _libraryLoadPromise = null; const BENCHMARK_SAMPLE_STORAGE_KEY = 'vcf-benchmark-sample-text'; const _libraryFilters = {text:'', lang:'', sex:'', type:'', rating:''}; const DEFAULT_BENCHMARK_SAMPLE_TEXT = 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.'; function benchmarkSampleText() { const el = $('benchmark-sample-text'); return (el && el.value.trim()) || DEFAULT_BENCHMARK_SAMPLE_TEXT; } function initBenchmarkSampleControls() { const sample = $('benchmark-sample-text'); if (!sample) return; sample.value = localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY) || DEFAULT_BENCHMARK_SAMPLE_TEXT; sample.addEventListener('input', debounce(() => { localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, sample.value.trim()); status('Benchmark sample sentence saved'); }, 500)); $('benchmark-reset-sample-btn')?.addEventListener('click', () => { sample.value = DEFAULT_BENCHMARK_SAMPLE_TEXT; localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, sample.value); status('Benchmark sample sentence reset'); }); $('benchmark-use-preview-btn')?.addEventListener('click', () => { const text = $('preview-text-area')?.value.trim(); if (!text) { toast('Preview text is empty', 'error'); return; } sample.value = text; localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, text); status('Benchmark sample sentence copied from TTS preview'); }); } // Returns the display-level source label for a voice. // Explicit origin field wins; fallback to any recognised tag value. function _displaySource(v) { if (v.origin) return v.origin; if (v.note) { // Notes from Rehearser clones follow: "Rehearser ·