From f469d428e2e113cc390bb6dde5db891f638e2da1 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 5 Jun 2026 15:07:09 +0200 Subject: [PATCH] Persist section reloads and reuse voice samples --- routes/stt.py | 37 ++++++++++++------- static/js/benchmark.js | 57 ++++++++++++++++++++++++++++-- static/loader.js | 18 +++++++--- static/nav.js | 39 +++++++++++++++++--- static/sections/s-performance.html | 11 ++++++ static/style.css | 5 +++ 6 files changed, 145 insertions(+), 22 deletions(-) diff --git a/routes/stt.py b/routes/stt.py index 9dde7db..547c97e 100644 --- a/routes/stt.py +++ b/routes/stt.py @@ -514,7 +514,8 @@ async def transcribe_bytes( except Exception as e: raise HTTPException(502, f"STT error: {e}") finally: - for p in {tmp, wav_tmp}: + cleanup = {p for p in (tmp, wav_tmp) if p is not None and p != _registry_get(source_id)} + for p in cleanup: try: p.unlink(missing_ok=True) except Exception: @@ -522,21 +523,32 @@ async def transcribe_bytes( @router.post("/api/stt-benchmark") async def stt_benchmark( - audio: UploadFile = File(...), + audio: UploadFile | None = File(None), + source_id: str = Form(""), reference_text: str = Form(""), reference_file: UploadFile | None = File(None), engines_json: str = Form(""), ): - suffix = Path(audio.filename or "audio.wav").suffix.lower() or ".wav" - if suffix not in (_AUDIO_EXTS | _UPLOAD_EXTS): - raise HTTPException(400, "Unsupported audio type") - tmp = TEMP_DIR / f"{uuid.uuid4().hex}_stt_bench{suffix}" - wav_tmp = tmp + tmp: Path | None = None + wav_tmp: Path | None = None try: - with tmp.open("wb") as f: - _copy_limited(audio.file, f, _MAX_UPLOAD_BYTES) - if suffix != ".wav": - wav_tmp = _to_wav_16k(tmp) + if source_id: + src = _registry_get(source_id) + if src is None or not src.exists(): + raise HTTPException(404, "Loaded voice sample not found") + wav_tmp = src + else: + if audio is None: + raise HTTPException(400, "Audio file or voice library sample is required") + suffix = Path(audio.filename or "audio.wav").suffix.lower() or ".wav" + if suffix not in (_AUDIO_EXTS | _UPLOAD_EXTS): + raise HTTPException(400, "Unsupported audio type") + tmp = TEMP_DIR / f"{uuid.uuid4().hex}_stt_bench{suffix}" + wav_tmp = tmp + with tmp.open("wb") as f: + _copy_limited(audio.file, f, _MAX_UPLOAD_BYTES) + if suffix != ".wav": + wav_tmp = _to_wav_16k(tmp) reference = (reference_text or "").strip() if reference_file is not None: raw = await reference_file.read() @@ -574,7 +586,8 @@ async def stt_benchmark( except Exception as e: raise HTTPException(500, f"STT benchmark failed: {e}") finally: - for p in {tmp, wav_tmp}: + cleanup = {p for p in (tmp, wav_tmp) if p is not None and p != _registry_get(source_id)} + for p in cleanup: try: p.unlink(missing_ok=True) except Exception: diff --git a/static/js/benchmark.js b/static/js/benchmark.js index 08e6938..3d410b8 100644 --- a/static/js/benchmark.js +++ b/static/js/benchmark.js @@ -431,6 +431,54 @@ bindBenchmarkModelPickers(); } + async function loadBenchmarkVoiceLibrary() { + const sel = q('bench-stt-library-voice'); + if (!sel) return; + sel.innerHTML = ''; + try { + const voices = await fetchJson('/api/voices'); + const usable = (voices || []).filter(v => v && v.enabled !== false && v.has_ref && (v.transcript || '').trim()); + sel.innerHTML = '' + usable + .sort((a, b) => String(a.display_name || a.id).localeCompare(String(b.display_name || b.id))) + .map(v => ``) + .join(''); + const note = q('bench-stt-source-note'); + if (note) note.textContent = usable.length ? `${usable.length} voices with reference text available.` : 'No library voices with reference transcripts found.'; + } catch (e) { + sel.innerHTML = ''; + const note = q('bench-stt-source-note'); + if (note) note.textContent = 'Could not load voice library: ' + e.message; + } + } + + async function useBenchmarkLibraryVoice() { + const sel = q('bench-stt-library-voice'); + const voiceId = sel?.value || ''; + if (!voiceId) { say('Choose a voice library sample first', 'error'); return; } + const btn = q('bench-stt-load-voice'); + const note = q('bench-stt-source-note'); + if (btn) btn.disabled = true; + if (note) note.textContent = 'Loading voice sample...'; + try { + const d = await fetchJson('/api/voice-load', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ voice_id: voiceId }), + }); + if (q('bench-stt-source-id')) q('bench-stt-source-id').value = d.id || ''; + if (q('bench-stt-ref-text')) q('bench-stt-ref-text').value = d.transcript || ''; + if (q('bench-stt-audio')) q('bench-stt-audio').value = ''; + if (q('bench-stt-ref-file')) q('bench-stt-ref-file').value = ''; + if (note) note.textContent = `${d.voice_id || voiceId} loaded from library${d.duration ? ` (${Number(d.duration).toFixed(1)}s)` : ''}.`; + say('Voice library sample loaded', 'success'); + } catch (e) { + if (note) note.textContent = 'Voice load failed.'; + say('Voice load failed: ' + e.message, 'error'); + } finally { + if (btn) btn.disabled = false; + } + } + function selectedSttEngines() { return [...document.querySelectorAll('.bench-stt-engine-check:checked')].map(cb => { const i = Number(cb.dataset.index); @@ -443,10 +491,11 @@ async function runSttBenchmark() { const audio = q('bench-stt-audio')?.files?.[0]; + const sourceId = q('bench-stt-source-id')?.value || ''; const refFile = q('bench-stt-ref-file')?.files?.[0]; const refText = q('bench-stt-ref-text')?.value || ''; const engines = selectedSttEngines(); - if (!audio) { say('Choose an audio file first', 'error'); return; } + if (!audio && !sourceId) { say('Choose an audio file or load a voice library sample first', 'error'); return; } if (!refFile && !refText.trim()) { say('Add a reference .txt or paste reference text', 'error'); return; } if (!engines.length) { say('Select at least one STT engine', 'error'); return; } const btn = q('bench-stt-run'); @@ -457,7 +506,8 @@ if (tbody) tbody.innerHTML = `Benchmarking ${engines.length} engines...`; try { const fd = new FormData(); - fd.append('audio', audio, audio.name); + if (audio) fd.append('audio', audio, audio.name); + if (sourceId) fd.append('source_id', sourceId); if (refFile) fd.append('reference_file', refFile, refFile.name); fd.append('reference_text', refText); fd.append('engines_json', JSON.stringify(engines)); @@ -733,6 +783,8 @@ document.querySelectorAll('.bench-tab').forEach(btn => btn.addEventListener('click', () => setBenchTab(btn.dataset.benchTab))); document.addEventListener('click', () => closeBenchmarkModelPickers()); q('bench-stt-refresh')?.addEventListener('click', loadBenchmarkSttEngines); + q('bench-stt-load-voice')?.addEventListener('click', useBenchmarkLibraryVoice); + q('bench-stt-audio')?.addEventListener('change', () => { if (q('bench-stt-source-id')) q('bench-stt-source-id').value = ''; }); q('bench-stt-run')?.addEventListener('click', runSttBenchmark); q('bench-tts-run')?.addEventListener('click', runTtsBenchmark); q('bench-turn-fetch-llm')?.addEventListener('click', fetchTurnModels); @@ -740,6 +792,7 @@ q('bench-turn-run')?.addEventListener('click', runTurnBenchmark); q('bench-turn-tts-backend')?.addEventListener('change', () => { if (q('bench-turn-voice')) q('bench-turn-voice').innerHTML = ''; }); loadBenchmarkSttEngines(); + loadBenchmarkVoiceLibrary(); initTurnControls(); } diff --git a/static/loader.js b/static/loader.js index 6dc35dd..9ca86b8 100644 --- a/static/loader.js +++ b/static/loader.js @@ -7,10 +7,20 @@ 's-llms', 's-conversation', ]; - // Determine which section to show first (same logic as nav.js) - var _savedSection = localStorage.getItem('ttsvc_section'); - var _activeSection = (_savedSection && SECTIONS.indexOf(_savedSection) !== -1) - ? _savedSection : 's-voices'; + function _storedSection() { + try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; } + } + function _hashSection() { + var raw = String(location.hash || '').replace(/^#/, '').trim(); + if (!raw) return ''; + if (raw.indexOf('section=') === 0) raw = raw.slice(8); + raw = decodeURIComponent(raw).replace(/^\/?/, ''); + return SECTIONS.indexOf(raw) !== -1 ? raw : ''; + } + + // Determine which section to show first (same logic as nav.js): hash > stored > My Voices. + var _savedSection = _storedSection(); + var _activeSection = _hashSection() || ((_savedSection && SECTIONS.indexOf(_savedSection) !== -1) ? _savedSection : 's-voices'); // ── Fetch app version once for JS cache-busting ────────────────────────── var _appVersion = 'dev'; diff --git a/static/nav.js b/static/nav.js index c207cc9..8426899 100644 --- a/static/nav.js +++ b/static/nav.js @@ -20,6 +20,30 @@ const SECTIONS = ['s-voices', 's-clone', 's-design', 's-studio', 's-tryout', 's-rehearser', 's-performance', 's-routing', 's-connect', 's-settings', 's-llms', 's-conversation']; + function storedSection() { + try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; } + } + + function setStoredSection(sectionId) { + try { localStorage.setItem('ttsvc_section', sectionId); } catch (_) {} + } + + function hashSection() { + var raw = String(location.hash || '').replace(/^#/, '').trim(); + if (!raw) return ''; + if (raw.indexOf('section=') === 0) raw = raw.slice(8); + raw = decodeURIComponent(raw).replace(/^\/?/, ''); + return SECTIONS.includes(raw) ? raw : ''; + } + + function setSectionHash(sectionId) { + if (!SECTIONS.includes(sectionId)) return; + var next = '#' + encodeURIComponent(sectionId); + if (location.hash === next) return; + try { history.replaceState(null, '', location.pathname + location.search + next); } + catch (_) { location.hash = next; } + } + function runSideEffects(name) { if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText(); if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary(); @@ -69,7 +93,9 @@ }; function showSection(sectionId) { - localStorage.setItem('ttsvc_section', sectionId); + if (!SECTIONS.includes(sectionId)) sectionId = 's-voices'; + setStoredSection(sectionId); + setSectionHash(sectionId); SECTIONS.forEach(function (id) { var el = document.getElementById(id); if (el) el.classList.toggle('is-active', id === sectionId); @@ -248,16 +274,21 @@ }); }; - // Restore last-used section from localStorage (fallback: My Voices) - var _savedSection = localStorage.getItem('ttsvc_section'); + // Restore last-used section from hash/localStorage (fallback: My Voices) + var _savedSection = storedSection(); // On very first visit, default voices tree open if (!localStorage.getItem('ttsvc_trees')) { _treeOpen['nav-voices-tree'] = true; } window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections'; window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm'; - var _startSection = (_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices'; + var _startSection = hashSection() || ((_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices'); showSection(_startSection); + + window.addEventListener('hashchange', function () { + var section = hashSection(); + if (section && section !== storedSection()) showSection(section); + }); var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library'; runSideEffects(_startTab); diff --git a/static/sections/s-performance.html b/static/sections/s-performance.html index 6567622..ac7cc7d 100644 --- a/static/sections/s-performance.html +++ b/static/sections/s-performance.html @@ -22,6 +22,17 @@ +
+
+ +
+ + +
+
Pick a library voice to use its WAV and reference transcript.
+
+
+
diff --git a/static/style.css b/static/style.css index a7a2405..c3bf92d 100644 --- a/static/style.css +++ b/static/style.css @@ -2385,6 +2385,11 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .bench-card-head h3 { margin: 0; font-size: 15px; color: var(--text); } .bench-card-head p { margin: 4px 0 0; font-size: 13px; color: var(--subtext); line-height: 1.45; max-width: 920px; } .bench-input-grid { align-items: end; } +.bench-library-source { margin: 2px 0 4px; max-width: 760px; } +.bench-source-row { display: flex; gap: 8px; align-items: center; } +.bench-source-row select { flex: 1; min-width: 220px; } +.bench-source-note { margin-top: 5px; color: var(--subtext); font-size: 12px; } +@media (max-width: 700px) { .bench-source-row { flex-direction: column; align-items: stretch; } } .bench-engine-panel { border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); overflow: hidden; } .bench-engine-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; } .bench-engine-head span { color: var(--subtext); }