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.
+
+
+