Persist section reloads and reuse voice samples

This commit is contained in:
mARTin-B78 2026-06-05 15:07:09 +02:00
parent 2547619998
commit f469d428e2
6 changed files with 145 additions and 22 deletions

View File

@ -514,7 +514,8 @@ async def transcribe_bytes(
except Exception as e: except Exception as e:
raise HTTPException(502, f"STT error: {e}") raise HTTPException(502, f"STT error: {e}")
finally: 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: try:
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
except Exception: except Exception:
@ -522,21 +523,32 @@ async def transcribe_bytes(
@router.post("/api/stt-benchmark") @router.post("/api/stt-benchmark")
async def stt_benchmark( async def stt_benchmark(
audio: UploadFile = File(...), audio: UploadFile | None = File(None),
source_id: str = Form(""),
reference_text: str = Form(""), reference_text: str = Form(""),
reference_file: UploadFile | None = File(None), reference_file: UploadFile | None = File(None),
engines_json: str = Form(""), engines_json: str = Form(""),
): ):
suffix = Path(audio.filename or "audio.wav").suffix.lower() or ".wav" tmp: Path | None = None
if suffix not in (_AUDIO_EXTS | _UPLOAD_EXTS): wav_tmp: Path | None = None
raise HTTPException(400, "Unsupported audio type")
tmp = TEMP_DIR / f"{uuid.uuid4().hex}_stt_bench{suffix}"
wav_tmp = tmp
try: try:
with tmp.open("wb") as f: if source_id:
_copy_limited(audio.file, f, _MAX_UPLOAD_BYTES) src = _registry_get(source_id)
if suffix != ".wav": if src is None or not src.exists():
wav_tmp = _to_wav_16k(tmp) 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() reference = (reference_text or "").strip()
if reference_file is not None: if reference_file is not None:
raw = await reference_file.read() raw = await reference_file.read()
@ -574,7 +586,8 @@ async def stt_benchmark(
except Exception as e: except Exception as e:
raise HTTPException(500, f"STT benchmark failed: {e}") raise HTTPException(500, f"STT benchmark failed: {e}")
finally: 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: try:
p.unlink(missing_ok=True) p.unlink(missing_ok=True)
except Exception: except Exception:

View File

@ -431,6 +431,54 @@
bindBenchmarkModelPickers(); bindBenchmarkModelPickers();
} }
async function loadBenchmarkVoiceLibrary() {
const sel = q('bench-stt-library-voice');
if (!sel) return;
sel.innerHTML = '<option value="">Loading voices...</option>';
try {
const voices = await fetchJson('/api/voices');
const usable = (voices || []).filter(v => v && v.enabled !== false && v.has_ref && (v.transcript || '').trim());
sel.innerHTML = '<option value="">-- choose from voice library --</option>' + usable
.sort((a, b) => String(a.display_name || a.id).localeCompare(String(b.display_name || b.id)))
.map(v => `<option value="${esc(v.id)}">${esc(v.display_name || v.name || v.id)}${v.duration ? ` (${Number(v.duration).toFixed(1)}s)` : ''}</option>`)
.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 = '<option value="">Voice library unavailable</option>';
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() { function selectedSttEngines() {
return [...document.querySelectorAll('.bench-stt-engine-check:checked')].map(cb => { return [...document.querySelectorAll('.bench-stt-engine-check:checked')].map(cb => {
const i = Number(cb.dataset.index); const i = Number(cb.dataset.index);
@ -443,10 +491,11 @@
async function runSttBenchmark() { async function runSttBenchmark() {
const audio = q('bench-stt-audio')?.files?.[0]; 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 refFile = q('bench-stt-ref-file')?.files?.[0];
const refText = q('bench-stt-ref-text')?.value || ''; const refText = q('bench-stt-ref-text')?.value || '';
const engines = selectedSttEngines(); 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 (!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; } if (!engines.length) { say('Select at least one STT engine', 'error'); return; }
const btn = q('bench-stt-run'); const btn = q('bench-stt-run');
@ -457,7 +506,8 @@
if (tbody) tbody.innerHTML = `<tr><td colspan="6" class="bench-empty">Benchmarking ${engines.length} engines...</td></tr>`; if (tbody) tbody.innerHTML = `<tr><td colspan="6" class="bench-empty">Benchmarking ${engines.length} engines...</td></tr>`;
try { try {
const fd = new FormData(); 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); if (refFile) fd.append('reference_file', refFile, refFile.name);
fd.append('reference_text', refText); fd.append('reference_text', refText);
fd.append('engines_json', JSON.stringify(engines)); 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.querySelectorAll('.bench-tab').forEach(btn => btn.addEventListener('click', () => setBenchTab(btn.dataset.benchTab)));
document.addEventListener('click', () => closeBenchmarkModelPickers()); document.addEventListener('click', () => closeBenchmarkModelPickers());
q('bench-stt-refresh')?.addEventListener('click', loadBenchmarkSttEngines); 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-stt-run')?.addEventListener('click', runSttBenchmark);
q('bench-tts-run')?.addEventListener('click', runTtsBenchmark); q('bench-tts-run')?.addEventListener('click', runTtsBenchmark);
q('bench-turn-fetch-llm')?.addEventListener('click', fetchTurnModels); q('bench-turn-fetch-llm')?.addEventListener('click', fetchTurnModels);
@ -740,6 +792,7 @@
q('bench-turn-run')?.addEventListener('click', runTurnBenchmark); q('bench-turn-run')?.addEventListener('click', runTurnBenchmark);
q('bench-turn-tts-backend')?.addEventListener('change', () => { if (q('bench-turn-voice')) q('bench-turn-voice').innerHTML = '<option value="">Fetch voices</option>'; }); q('bench-turn-tts-backend')?.addEventListener('change', () => { if (q('bench-turn-voice')) q('bench-turn-voice').innerHTML = '<option value="">Fetch voices</option>'; });
loadBenchmarkSttEngines(); loadBenchmarkSttEngines();
loadBenchmarkVoiceLibrary();
initTurnControls(); initTurnControls();
} }

View File

@ -7,10 +7,20 @@
's-llms', 's-conversation', 's-llms', 's-conversation',
]; ];
// Determine which section to show first (same logic as nav.js) function _storedSection() {
var _savedSection = localStorage.getItem('ttsvc_section'); try { return localStorage.getItem('ttsvc_section') || ''; } catch (_) { return ''; }
var _activeSection = (_savedSection && SECTIONS.indexOf(_savedSection) !== -1) }
? _savedSection : 's-voices'; 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 ────────────────────────── // ── Fetch app version once for JS cache-busting ──────────────────────────
var _appVersion = 'dev'; var _appVersion = 'dev';

View File

@ -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']; 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) { function runSideEffects(name) {
if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText(); if ((name === 'source' || name === 'save') && typeof initCloneSampleText === 'function') initCloneSampleText();
if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary(); if (name === 'library' && typeof loadVoiceLibrary === 'function') loadVoiceLibrary();
@ -69,7 +93,9 @@
}; };
function showSection(sectionId) { function showSection(sectionId) {
localStorage.setItem('ttsvc_section', sectionId); if (!SECTIONS.includes(sectionId)) sectionId = 's-voices';
setStoredSection(sectionId);
setSectionHash(sectionId);
SECTIONS.forEach(function (id) { SECTIONS.forEach(function (id) {
var el = document.getElementById(id); var el = document.getElementById(id);
if (el) el.classList.toggle('is-active', id === sectionId); if (el) el.classList.toggle('is-active', id === sectionId);
@ -248,16 +274,21 @@
}); });
}; };
// Restore last-used section from localStorage (fallback: My Voices) // Restore last-used section from hash/localStorage (fallback: My Voices)
var _savedSection = localStorage.getItem('ttsvc_section'); var _savedSection = storedSection();
// On very first visit, default voices tree open // On very first visit, default voices tree open
if (!localStorage.getItem('ttsvc_trees')) { if (!localStorage.getItem('ttsvc_trees')) {
_treeOpen['nav-voices-tree'] = true; _treeOpen['nav-voices-tree'] = true;
} }
window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections'; window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections';
window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm'; 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); 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'; var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library';
runSideEffects(_startTab); runSideEffects(_startTab);

View File

@ -22,6 +22,17 @@
</div> </div>
<button class="btn-secondary" id="bench-stt-refresh" type="button"><span class="mdi mdi-refresh"></span> Refresh engines</button> <button class="btn-secondary" id="bench-stt-refresh" type="button"><span class="mdi mdi-refresh"></span> Refresh engines</button>
</div> </div>
<div class="bench-library-source">
<div class="s-field">
<label>Use voice library sample</label>
<div class="bench-source-row">
<select id="bench-stt-library-voice"><option value="">Loading voices...</option></select>
<button class="btn-secondary" id="bench-stt-load-voice" type="button"><span class="mdi mdi-database-import-outline"></span> Use voice</button>
</div>
<div class="bench-source-note" id="bench-stt-source-note">Pick a library voice to use its WAV and reference transcript.</div>
</div>
</div>
<input id="bench-stt-source-id" type="hidden" value="">
<div class="settings-grid three bench-input-grid"> <div class="settings-grid three bench-input-grid">
<div class="s-field"> <div class="s-field">
<label>Audio (.wav or audio file)</label> <label>Audio (.wav or audio file)</label>

View File

@ -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 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-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-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-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 { 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); } .bench-engine-head span { color: var(--subtext); }