diff --git a/static/js/benchmark.js b/static/js/benchmark.js
index 1d0d9f8..0b42709 100644
--- a/static/js/benchmark.js
+++ b/static/js/benchmark.js
@@ -1,26 +1,30 @@
// ── 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');
+ const batchBackendSel = $('batch-backend-select');
+ const batchRunsSel = $('batch-runs');
+ const batchLoadBtn = $('batch-load-voices-btn');
+ const batchSearchInput = $('batch-voice-search');
+ 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 perfRunSelectedBtn = $('perf-run-selected-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 = [];
+ let batchVoices = [];
+ let batchSelected = new Set();
function populateBatchBackends() {
if (!batchBackendSel) return;
@@ -31,66 +35,172 @@
}
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 libraryVoice(id) {
+ return (window._voices || []).find(v => v && v.id === id) || null;
}
- function buildVoiceList(voices) {
- const activeIds = new Set(activeVoiceIds());
- if (!voices.length) {
- batchVoiceList.innerHTML = 'No voices found.
';
+ function normalizeBatchVoice(value) {
+ const id = typeof value === 'string' ? value : (value?.id || value?.voice || value?.name || value?.voice_id || '');
+ const meta = typeof value === 'object' ? (value.meta || libraryVoice(id) || value) : (libraryVoice(id) || null);
+ return id ? {
+ id,
+ label: meta?.display_name || value?.label || meta?.name || value?.name || id,
+ meta,
+ } : null;
+ }
+
+ function batchVoiceStats(v) {
+ const meta = v?.meta || libraryVoice(v?.id) || {};
+ const b = meta.benchmark || {};
+ const parts = [];
+ const dur = Number(meta.duration);
+ const speed = Number(b.speed ?? b.avg_speed);
+ const rtf = Number(b.rtf ?? b.avg_rtf);
+ if (Number.isFinite(dur) && dur > 0) parts.push(`${dur.toFixed(1)}s`);
+ if (Number.isFinite(speed) && speed > 0) parts.push(`${speed.toFixed(2)}x`);
+ else if (Number.isFinite(rtf) && rtf > 0) parts.push(`RTF ${rtf.toFixed(2)}`);
+ return parts.join(' · ');
+ }
+
+ function batchColor(id) {
+ const palette = ['#3b82f6', '#10b981', '#8b5cf6', '#f59e0b', '#ef4444', '#ec4899', '#06b6d4', '#84cc16'];
+ let h = 0;
+ for (let i = 0; i < String(id || '').length; i++) h = (h * 31 + String(id).charCodeAt(i)) >>> 0;
+ return palette[h % palette.length];
+ }
+
+ function batchAvatar(v) {
+ const meta = v?.meta || libraryVoice(v?.id) || {};
+ if (meta.has_picture) return `
`;
+ const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta.avatar, 28) : null;
+ if (icon) return icon.replace(/vp-avatar/g, 'batch-voice-avatar');
+ const init = (v.label || v.id || '?')[0].toUpperCase();
+ return `${escHtml(init)}`;
+ }
+
+ function batchSearchText(v) {
+ const meta = v.meta || {};
+ return [v.id, v.label, meta.display_name, meta.name, meta.lang, meta.language, Array.isArray(meta.tags) ? meta.tags.join(' ') : meta.tags, batchVoiceStats(v)]
+ .filter(Boolean).join(' ').toLowerCase();
+ }
+
+ function syncRunSelectedLabel() {
+ const count = batchSelected.size;
+ if (perfRunSelectedBtn) {
+ perfRunSelectedBtn.disabled = count === 0;
+ perfRunSelectedBtn.innerHTML = ` ${count ? `Run ${count} selected voice${count === 1 ? '' : 's'}` : 'Run selected voices'}`;
+ }
+ }
+
+ function updateSelCount() {
+ const total = batchVoices.length;
+ const checked = batchSelected.size;
+ const visible = filteredBatchVoices().length;
+ if (batchSelCount) batchSelCount.textContent = total ? `${checked} of ${total} selected${visible !== total ? ` · ${visible} shown` : ''}` : '';
+ batchRunBtn.disabled = checked === 0;
+ syncRunSelectedLabel();
+ }
+
+ function filteredBatchVoices() {
+ const q = (batchSearchInput?.value || '').trim().toLowerCase();
+ return q ? batchVoices.filter(v => batchSearchText(v).includes(q)) : batchVoices;
+ }
+
+ function renderBatchVoiceList() {
+ if (!batchVoiceList) return;
+ const visible = filteredBatchVoices();
+ if (!batchVoices.length) {
+ batchVoiceList.innerHTML = 'No voices found. Fetch voices above or reload from backend.
';
updateSelCount();
return;
}
- batchVoiceList.innerHTML = voices.map(v => {
+ if (!visible.length) {
+ batchVoiceList.innerHTML = 'No matching voices.
';
+ updateSelCount();
+ return;
+ }
+ const activeIds = new Set(activeVoiceIds());
+ batchVoiceList.innerHTML = visible.map(v => {
+ const checked = batchSelected.has(v.id);
const isActive = activeIds.has(v.id);
- return `