tts-voice-creator-clone-and.../static/js/benchmark.js
mARTin-B78 c7a1e35539 Security audit, modular refactor, and container-name field
Security fixes:
- Block /proc /sys /dev /run /boot in /api/browse-dirs (path traversal)
- Verify yt-dlp output stays inside TEMP_DIR before registration
- Remove Access-Control-Allow-Origin: * from /api/proxy-audio
- TTL-based temp file registry (default 2h) to prevent disk fill

Performance:
- Cache settings + routing rules in memory (mtime-checked); eliminates
  per-request disk reads on every TTS call

UI:
- Add container name (optional) field to Docker stack TTS/STT engine
  cards (Qwen3 Voice Clone, Voice Design, Custom Voice, Streaming,
  NVIDIA Magpie, Parakeet) — enables Stop/Start/Restart buttons on
  all engine cards, matching the existing Other Local TTS/STT cards

Refactor — backend:
- server.py: 5560 lines → 43-line entry point
- core/ package: constants, registry, validation, docker_client,
  config, routing, audio, voice, presets, tts_helpers
- routes/ package: admin, settings, library, stt, sources, docker,
  tts, conversation (FastAPI APIRouter modules)
- Dockerfile + docker-compose.yml updated to include core/ and routes/

Refactor — frontend:
- static/app.js: 8744 lines → 16 modules in static/js/
  utils, voice-inspector, voice-sources, integrations, routing,
  settings, voice-clone, voice-library, tts-preview, benchmark,
  stt, init, engines, ai-backends, generation, conversation
- static/loader.js updated to load modules sequentially

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 12:13:07 +02:00

219 lines
9.9 KiB
JavaScript

// ── Batch benchmark ───────────────────────────────────────────────────────
(function initBatchBenchmark() {
const batchBackendSel = $('batch-backend-select');
const batchRunsSel = $('batch-runs');
const batchLoadBtn = $('batch-load-voices-btn');
const batchSelectAllBtn = $('batch-select-all-btn');
const batchSelectNoneBtn= $('batch-select-none-btn');
const batchVoiceList = $('batch-voice-list');
const batchSelCount = $('batch-selected-count');
const batchRunBtn = $('batch-run-btn');
const batchStopBtn = $('batch-stop-btn');
const batchProgress = $('batch-progress');
const batchProgLabel = $('batch-progress-label');
const batchProgCount = $('batch-progress-count');
const batchProgBar = $('batch-progress-bar');
const batchResultsCard = $('batch-results-card');
const batchResultsLabel = $('batch-results-label');
const batchTbody = $('batch-tbody');
if (!batchRunBtn) return;
let batchStopped = false;
let batchResults = [];
function populateBatchBackends() {
if (!batchBackendSel) return;
const cur = batchBackendSel.value;
batchBackendSel.innerHTML = availableTtsBackends().map(b =>
`<option value="${escHtml(b.id)}"${b.id===cur?' selected':''}>${escHtml(b.label)}</option>`
).join('') || '<option value="">No backends available</option>';
}
populateBatchBackends();
function updateSelCount() {
if (!batchVoiceList || !batchSelCount) return;
const total = batchVoiceList.querySelectorAll('.batch-voice-cb').length;
const checked = batchVoiceList.querySelectorAll('.batch-voice-cb:checked').length;
batchSelCount.textContent = total ? `${checked} of ${total} selected` : '';
batchRunBtn.disabled = checked === 0;
}
function buildVoiceList(voices) {
const activeIds = new Set(activeVoiceIds());
if (!voices.length) {
batchVoiceList.innerHTML = '<div class="perf-history-empty">No voices found.</div>';
updateSelCount();
return;
}
batchVoiceList.innerHTML = voices.map(v => {
const isActive = activeIds.has(v.id);
return `<label class="batch-voice-item${isActive ? ' is-active' : ''}">
<input type="checkbox" class="batch-voice-cb" value="${escHtml(v.id)}"${isActive ? ' checked' : ''}>
<span class="batch-voice-name">${escHtml(v.label)}</span>
${isActive ? '<span class="batch-voice-tag">active</span>' : ''}
</label>`;
}).join('');
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.addEventListener('change', updateSelCount));
updateSelCount();
}
// Pre-populate from My Voices library on init
function populateFromLibrary() {
const voices = (_voices || [])
.filter(v => v.enabled !== false)
.map(v => ({ id: v.id, label: v.display_name || v.name || v.id }))
.sort((a, b) => a.label.localeCompare(b.label));
buildVoiceList(voices);
}
populateFromLibrary();
batchLoadBtn.addEventListener('click', async () => {
const backend = batchBackendSel.value;
if (!backend) { toast('Select a backend first', 'error'); return; }
batchLoadBtn.disabled = true;
batchVoiceList.innerHTML = '<div class="perf-history-empty">Loading from backend…</div>';
try {
const raw = await fetch('/api/tts-voices?backend=' + encodeURIComponent(backend)).then(r => r.json());
const voices = raw.map(v => ({ id: backendVoiceId(v), label: backendVoiceId(v) }))
.sort((a, b) => a.label.localeCompare(b.label));
buildVoiceList(voices);
} catch(e) {
batchVoiceList.innerHTML = `<div class="perf-history-empty">Failed: ${escHtml(e.message)}</div>`;
}
finally { batchLoadBtn.disabled = false; }
});
batchSelectAllBtn.addEventListener('click', () => {
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.checked = true);
updateSelCount();
});
batchSelectNoneBtn.addEventListener('click', () => {
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.checked = false);
updateSelCount();
});
function renderBatchResults() {
if (!batchResults.length) { batchResultsCard.style.display = 'none'; return; }
batchResultsCard.style.display = '';
const sorted = batchResults.slice().sort((a, b) => {
if (a.ok && !b.ok) return -1;
if (!a.ok && b.ok) return 1;
return (a.avgRtf || 999) - (b.avgRtf || 999);
});
const okCount = batchResults.filter(r => r.ok).length;
if (batchResultsLabel) batchResultsLabel.textContent = `${okCount} / ${batchResults.length} voices — ${batchBackendSel.value}`;
batchTbody.innerHTML = sorted.map(r => {
const rtfCls = r.ok && r.avgRtf < 1 ? 'perf-good' : r.ok ? 'perf-slow' : '';
const trend = r.trend ? `<span class="perf-trend-badge ${r.trend.cls}" style="font-size:11px;padding:1px 6px">${r.trend.label}</span>` : '';
return `<tr class="${r.ok ? '' : 'perf-row-error'}">
<td>${escHtml(r.voice)}</td>
<td>${r.ok ? Math.round(r.avgLatency) + ' ms' : '—'}</td>
<td>${r.ok ? r.minLatency + ' ms' : '—'}</td>
<td>${r.ok && r.avgAudio > 0 ? r.avgAudio.toFixed(2) : '—'}</td>
<td class="${rtfCls}">${r.ok && r.avgRtf ? r.avgRtf.toFixed(2) : '—'}${trend}</td>
<td>${r.ok ? '<span class="perf-ok">OK</span>' : `<span class="perf-err">${escHtml(r.error || 'Failed')}</span>`}</td>
</tr>`;
}).join('');
}
batchRunBtn.addEventListener('click', async () => {
const backend = batchBackendSel.value;
const text = $('perf-text')?.value.trim();
const runs = parseInt(batchRunsSel.value) || 1;
const selected = [...(batchVoiceList?.querySelectorAll('.batch-voice-cb:checked') || [])].map(cb => cb.value);
if (!backend) { toast('Select a backend first', 'error'); return; }
if (!text) { toast('Enter sample text in the single-voice form above', 'error'); return; }
if (!selected.length) { toast('Select at least one voice', 'error'); return; }
batchStopped = false;
batchResults = [];
batchRunBtn.disabled = true;
batchStopBtn.disabled = false;
batchProgress.style.display = '';
batchResultsCard.style.display = 'none';
for (let vi = 0; vi < selected.length; vi++) {
if (batchStopped) break;
const voice = selected[vi];
batchProgLabel.textContent = `${voice} (${vi + 1} / ${selected.length})`;
batchProgCount.textContent = `${vi + 1} / ${selected.length}`;
batchProgBar.style.width = `${Math.round((vi / selected.length) * 100)}%`;
const entry = { voice, ok: false, avgLatency: 0, minLatency: 0, avgRtf: 0, avgAudio: 0, error: '' };
const rowRunResults = [];
for (let ri = 0; ri < runs; ri++) {
if (batchStopped) break;
try {
const t0 = performance.now();
const blob = await fetchTtsPreviewBlob(voice, text, 'wav', '', backend);
const lat = Math.round(performance.now() - t0);
let dur = 0;
try {
const ac = new (window.AudioContext || window.webkitAudioContext)();
const buf = await ac.decodeAudioData(await blob.arrayBuffer());
dur = buf.duration; ac.close();
} catch(_) {}
rowRunResults.push({ lat, dur });
} catch(e) { entry.error = e.message; break; }
}
if (rowRunResults.length) {
entry.ok = true;
entry.avgLatency = rowRunResults.reduce((s, r) => s + r.lat, 0) / rowRunResults.length;
entry.minLatency = Math.min(...rowRunResults.map(r => r.lat));
const durRows = rowRunResults.filter(r => r.dur > 0);
entry.avgAudio = durRows.length ? durRows.reduce((s, r) => s + r.dur, 0) / durRows.length : 0;
const rtfArr = durRows.map(r => r.lat / 1000 / r.dur);
entry.avgRtf = rtfArr.length ? rtfArr.reduce((s, v) => s + v, 0) / rtfArr.length : 0;
// compute trend vs previous session for this voice
if (entry.avgRtf > 0) {
const prev = perfHistoryLoad().filter(e => e.backend === backend && e.voice === voice && typeof e.avgRtf === 'number');
if (prev.length) {
const prevRtf = prev[prev.length - 1].avgRtf;
const delta = entry.avgRtf - prevRtf;
const pct = Math.abs(delta / Math.max(prevRtf, 0.01)) * 100;
if (pct < 5) entry.trend = { cls: 'perf-trend-stable', label: '→ stable' };
else if (delta < 0) entry.trend = { cls: 'perf-trend-better', label: `${pct.toFixed(0)}% faster` };
else entry.trend = { cls: 'perf-trend-worse', label: `${pct.toFixed(0)}% slower` };
}
perfHistoryAdd({
ts: Date.now(), backend, voice, textLen: text.length,
avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency,
maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)),
avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true,
});
}
}
batchResults.push(entry);
renderBatchResults();
}
batchProgBar.style.width = '100%';
batchProgLabel.textContent = batchStopped
? `Stopped after ${batchResults.length} voice${batchResults.length !== 1 ? 's' : ''}.`
: `Done — ${batchResults.length} voice${batchResults.length !== 1 ? 's' : ''} benchmarked.`;
batchStopBtn.disabled = true;
batchRunBtn.disabled = false;
renderPerfHistory();
const ok = batchResults.filter(r => r.ok);
const best = ok.slice().sort((a, b) => a.avgRtf - b.avgRtf)[0];
toast(
`Batch done: ${ok.length}/${batchResults.length} OK` +
(best ? `, best RTF ${best.avgRtf.toFixed(2)} (${best.voice})` : ''),
ok.length < batchResults.length ? 'error' : 'success'
);
});
batchStopBtn.addEventListener('click', () => {
batchStopped = true;
batchStopBtn.disabled = true;
batchProgLabel.textContent = 'Stopping after current voice…';
});
})();