// ── 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 _VL_CACHE_KEY = 'ttsvc_vc'; function _vlCacheRead() { try { return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY) || 'null'); } catch (_) { return null; } } function _vlCacheWrite(voices) { try { sessionStorage.setItem(_VL_CACHE_KEY, JSON.stringify(voices)); } catch (_) {} } function _vlCacheClear() { try { sessionStorage.removeItem(_VL_CACHE_KEY); } catch (_) {} } window._vlCacheClear = _vlCacheClear; const _libraryFilters = {text:'', lang:'', sex:'', type:'', rating:''}; let _libraryFilterOptionsSig = ''; const DEFAULT_BENCHMARK_SAMPLE_TEXT = 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.'; const BENCHMARK_PRESETS = { de: 'Die Welt ist voller Geschichten, die darauf warten, erzählt zu werden — von mutigen Helden und stillen Träumern.', en: 'The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.', de2: 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.', en2: 'She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.', reset: DEFAULT_BENCHMARK_SAMPLE_TEXT, }; 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'); }); const presetSel = $('benchmark-preset-select'); if (presetSel) { presetSel.addEventListener('change', () => { const key = presetSel.value; if (!key || !BENCHMARK_PRESETS[key]) { presetSel.value = ''; return; } sample.value = BENCHMARK_PRESETS[key]; localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, sample.value); presetSel.value = ''; status('Benchmark sample sentence loaded'); }); } $('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 ·