Persist section reloads and reuse voice samples
This commit is contained in:
parent
2547619998
commit
f469d428e2
@ -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,17 +523,28 @@ 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(""),
|
||||||
):
|
):
|
||||||
|
tmp: Path | None = None
|
||||||
|
wav_tmp: Path | None = None
|
||||||
|
try:
|
||||||
|
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"
|
suffix = Path(audio.filename or "audio.wav").suffix.lower() or ".wav"
|
||||||
if suffix not in (_AUDIO_EXTS | _UPLOAD_EXTS):
|
if suffix not in (_AUDIO_EXTS | _UPLOAD_EXTS):
|
||||||
raise HTTPException(400, "Unsupported audio type")
|
raise HTTPException(400, "Unsupported audio type")
|
||||||
tmp = TEMP_DIR / f"{uuid.uuid4().hex}_stt_bench{suffix}"
|
tmp = TEMP_DIR / f"{uuid.uuid4().hex}_stt_bench{suffix}"
|
||||||
wav_tmp = tmp
|
wav_tmp = tmp
|
||||||
try:
|
|
||||||
with tmp.open("wb") as f:
|
with tmp.open("wb") as f:
|
||||||
_copy_limited(audio.file, f, _MAX_UPLOAD_BYTES)
|
_copy_limited(audio.file, f, _MAX_UPLOAD_BYTES)
|
||||||
if suffix != ".wav":
|
if suffix != ".wav":
|
||||||
@ -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:
|
||||||
|
|||||||
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user