Improve benchmark voice selection and device reporting
This commit is contained in:
parent
7b855394a5
commit
1f4784653c
@ -79,6 +79,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a class="skip-link" href="#main-content">Skip to main content</a>
|
||||
|
||||
<!-- Hidden tabs nav — required for JS backend availability detection -->
|
||||
<nav class="tabs" style="display:none" aria-hidden="true">
|
||||
@ -186,7 +187,7 @@
|
||||
</aside>
|
||||
|
||||
<!-- Main content — sections injected by loader.js -->
|
||||
<main id="main-content">
|
||||
<main id="main-content" tabindex="-1">
|
||||
|
||||
<!-- ── Skeleton loading view ─────────────────────────────────────────── -->
|
||||
<!-- Visible immediately; loader.js fades it out when real content is ready -->
|
||||
|
||||
@ -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 = '<div class="perf-history-empty">No voices found.</div>';
|
||||
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 `<img class="batch-voice-avatar" src="/api/voice/picture/${encodeURIComponent(v.id)}" alt="">`;
|
||||
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 `<span class="batch-voice-avatar batch-voice-avatar-init" style="background:${batchColor(meta.lang || v.id)}">${escHtml(init)}</span>`;
|
||||
}
|
||||
|
||||
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 = `<span class="mdi mdi-playlist-play"></span> ${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 = '<div class="perf-history-empty">No voices found. Fetch voices above or reload from backend.</div>';
|
||||
updateSelCount();
|
||||
return;
|
||||
}
|
||||
batchVoiceList.innerHTML = voices.map(v => {
|
||||
if (!visible.length) {
|
||||
batchVoiceList.innerHTML = '<div class="perf-history-empty">No matching voices.</div>';
|
||||
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 `<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>
|
||||
const stats = batchVoiceStats(v);
|
||||
return `<label class="batch-voice-item${isActive ? ' is-active' : ''}${checked ? ' is-selected' : ''}">
|
||||
<input type="checkbox" class="batch-voice-cb" value="${escHtml(v.id)}" aria-label="Benchmark ${escHtml(v.label || v.id)}"${checked ? ' checked' : ''}>
|
||||
${batchAvatar(v)}
|
||||
<span class="batch-voice-main"><span class="batch-voice-name">${escHtml(v.label || v.id)}</span><span class="batch-voice-id">${escHtml(v.id)}</span></span>
|
||||
${stats ? `<span class="batch-voice-stats">${escHtml(stats)}</span>` : ''}
|
||||
${isActive ? '<span class="batch-voice-tag">active</span>' : ''}
|
||||
</label>`;
|
||||
}).join('');
|
||||
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.addEventListener('change', updateSelCount));
|
||||
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.addEventListener('change', () => {
|
||||
if (cb.checked) batchSelected.add(cb.value);
|
||||
else batchSelected.delete(cb.value);
|
||||
renderBatchVoiceList();
|
||||
}));
|
||||
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);
|
||||
function buildVoiceList(voices, opts = {}) {
|
||||
const previous = opts.preserve ? new Set(batchSelected) : null;
|
||||
const byId = new Map();
|
||||
(voices || []).map(normalizeBatchVoice).filter(Boolean).forEach(v => byId.set(v.id, v));
|
||||
batchVoices = Array.from(byId.values()).sort((a, b) => (a.label || a.id).localeCompare(b.label || b.id));
|
||||
if (previous) {
|
||||
batchSelected = new Set(batchVoices.filter(v => previous.has(v.id)).map(v => v.id));
|
||||
} else if (opts.keepSelection) {
|
||||
batchSelected = new Set(Array.from(batchSelected).filter(id => byId.has(id)));
|
||||
} else {
|
||||
const activeIds = new Set(activeVoiceIds());
|
||||
batchSelected = new Set(batchVoices.filter(v => activeIds.has(v.id)).map(v => v.id));
|
||||
}
|
||||
renderBatchVoiceList();
|
||||
}
|
||||
|
||||
async function populateFromLibrary() {
|
||||
try {
|
||||
if ((!window._voices || !window._voices.length) && typeof loadVoiceLibrary === 'function') await loadVoiceLibrary();
|
||||
const voices = (window._voices || []).filter(v => v.enabled !== false).map(v => ({ id: v.id, label: v.display_name || v.name || v.id, meta: v }));
|
||||
buildVoiceList(voices);
|
||||
} catch (e) {
|
||||
if (batchVoiceList) batchVoiceList.innerHTML = `<div class="perf-history-empty">Voice library unavailable: ${escHtml(e.message)}</div>`;
|
||||
updateSelCount();
|
||||
}
|
||||
}
|
||||
populateFromLibrary();
|
||||
|
||||
batchLoadBtn.addEventListener('click', async () => {
|
||||
async function loadBatchVoicesFromBackend() {
|
||||
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>';
|
||||
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));
|
||||
const voices = raw.map(v => {
|
||||
const id = backendVoiceId(v);
|
||||
return { id, label: id, meta: libraryVoice(id) || (typeof v === 'object' ? v : null) };
|
||||
}).filter(v => v.id);
|
||||
buildVoiceList(voices);
|
||||
} catch(e) {
|
||||
batchVoiceList.innerHTML = `<div class="perf-history-empty">Failed: ${escHtml(e.message)}</div>`;
|
||||
updateSelCount();
|
||||
}
|
||||
finally { batchLoadBtn.disabled = false; }
|
||||
});
|
||||
}
|
||||
|
||||
batchLoadBtn.addEventListener('click', loadBatchVoicesFromBackend);
|
||||
batchSearchInput?.addEventListener('input', renderBatchVoiceList);
|
||||
batchSelectAllBtn.addEventListener('click', () => {
|
||||
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.checked = true);
|
||||
updateSelCount();
|
||||
batchVoices.forEach(v => batchSelected.add(v.id));
|
||||
renderBatchVoiceList();
|
||||
});
|
||||
batchSelectNoneBtn.addEventListener('click', () => {
|
||||
batchVoiceList.querySelectorAll('.batch-voice-cb').forEach(cb => cb.checked = false);
|
||||
updateSelCount();
|
||||
batchSelected.clear();
|
||||
renderBatchVoiceList();
|
||||
});
|
||||
window.addEventListener('benchmark:tts-voices-fetched', e => {
|
||||
const detail = e.detail || {};
|
||||
if (detail.backend && batchBackendSel) batchBackendSel.value = detail.backend;
|
||||
buildVoiceList(detail.voices || []);
|
||||
});
|
||||
|
||||
function renderBatchResults() {
|
||||
@ -106,8 +216,11 @@
|
||||
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>` : '';
|
||||
const device = typeof backendComputeDevice === 'function' ? backendComputeDevice(r.backend || batchBackendSel.value) : 'Unknown';
|
||||
const deviceCls = typeof backendComputeDeviceClass === 'function' ? backendComputeDeviceClass(r.backend || batchBackendSel.value) : '';
|
||||
return `<tr class="${r.ok ? '' : 'perf-row-error'}">
|
||||
<td>${escHtml(r.voice)}</td>
|
||||
<td><span class="bench-device ${deviceCls}" title="Inferred from the selected TTS backend metadata">${escHtml(device)}</span></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>
|
||||
@ -117,18 +230,30 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
batchRunBtn.addEventListener('click', async () => {
|
||||
async function runBatchBenchmark() {
|
||||
const topBackend = $('perf-backend-select')?.value || '';
|
||||
if (topBackend && batchBackendSel && batchBackendSel.value !== topBackend) batchBackendSel.value = topBackend;
|
||||
const topRuns = $('perf-runs')?.value || '';
|
||||
if (topRuns && batchRunsSel && Array.from(batchRunsSel.options).some(o => o.value === topRuns)) batchRunsSel.value = topRuns;
|
||||
const singleVoice = $('perf-voice-select')?.value || '';
|
||||
if (!batchSelected.size && singleVoice && batchVoices.some(v => v.id === singleVoice)) {
|
||||
batchSelected.add(singleVoice);
|
||||
renderBatchVoiceList();
|
||||
}
|
||||
|
||||
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);
|
||||
const validIds = new Set(batchVoices.map(v => v.id));
|
||||
const selected = Array.from(batchSelected).filter(id => validIds.has(id));
|
||||
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; }
|
||||
if (!selected.length) { toast('Select one or more voices first', 'error'); return; }
|
||||
|
||||
batchStopped = false;
|
||||
batchResults = [];
|
||||
batchRunBtn.disabled = true;
|
||||
if (perfRunSelectedBtn) perfRunSelectedBtn.disabled = true;
|
||||
batchStopBtn.disabled = false;
|
||||
batchProgress.style.display = '';
|
||||
batchResultsCard.style.display = 'none';
|
||||
@ -140,7 +265,7 @@
|
||||
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 entry = { voice, backend, ok: false, avgLatency: 0, minLatency: 0, avgRtf: 0, avgAudio: 0, error: '' };
|
||||
const rowRunResults = [];
|
||||
|
||||
for (let ri = 0; ri < runs; ri++) {
|
||||
@ -161,23 +286,22 @@
|
||||
|
||||
if (rowRunResults.length) {
|
||||
entry.ok = true;
|
||||
entry.avgLatency = rowRunResults.reduce((s, r) => s + r.lat, 0) / rowRunResults.length;
|
||||
entry.avgLatency = rowRunResults.reduce((sum, r) => sum + 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;
|
||||
entry.avgAudio = durRows.length ? durRows.reduce((sum, r) => sum + 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;
|
||||
entry.avgRtf = rtfArr.length ? rtfArr.reduce((sum, value) => sum + value, 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` };
|
||||
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,
|
||||
@ -192,27 +316,30 @@
|
||||
renderBatchResults();
|
||||
}
|
||||
|
||||
batchProgBar.style.width = '100%';
|
||||
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.`;
|
||||
: `Done - ${batchResults.length} voice${batchResults.length !== 1 ? 's' : ''} benchmarked.`;
|
||||
batchStopBtn.disabled = true;
|
||||
batchRunBtn.disabled = false;
|
||||
batchRunBtn.disabled = false;
|
||||
syncRunSelectedLabel();
|
||||
renderPerfHistory();
|
||||
|
||||
const ok = batchResults.filter(r => r.ok);
|
||||
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'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
batchRunBtn.addEventListener('click', runBatchBenchmark);
|
||||
perfRunSelectedBtn?.addEventListener('click', runBatchBenchmark);
|
||||
batchStopBtn.addEventListener('click', () => {
|
||||
batchStopped = true;
|
||||
batchStopBtn.disabled = true;
|
||||
batchProgLabel.textContent = 'Stopping after current voice…';
|
||||
batchProgLabel.textContent = 'Stopping after current voice...';
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
@ -29,6 +29,24 @@ function backendById(id) {
|
||||
return availableTtsBackends().find(b => b.id === id) || availableTtsBackends()[0] || null;
|
||||
}
|
||||
|
||||
function backendComputeDevice(id) {
|
||||
const b = backendById(id);
|
||||
const text = [id, b?.label, b?.speed, b?.latency, b?.quality, b?.ram, b?.purpose, b?.best_for]
|
||||
.filter(Boolean).join(' ').toLowerCase();
|
||||
if (/(cloud|api|elevenlabs|groq)/.test(text)) return 'Cloud';
|
||||
if (/(cpu|metal)/.test(text) && !/(cuda|gpu|vram|rtx|dgx)/.test(text)) return 'CPU';
|
||||
if (/(cuda|gpu|vram|rtx|dgx)/.test(text)) return 'CUDA/GPU';
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
function backendComputeDeviceClass(id) {
|
||||
const label = backendComputeDevice(id).toLowerCase();
|
||||
if (label.includes('cuda') || label.includes('gpu')) return 'gpu';
|
||||
if (label.includes('cpu')) return 'cpu';
|
||||
if (label.includes('cloud')) return 'cloud';
|
||||
return '';
|
||||
}
|
||||
|
||||
function backendHelpHtml(b, compact = false) {
|
||||
if (!b) return '<strong>No TTS backend is reachable.</strong><div>Start at least one TTS service or check Settings URLs.</div>';
|
||||
const tags = [
|
||||
|
||||
@ -409,6 +409,7 @@ function renderPerfHistory() {
|
||||
} else {
|
||||
perfVoiceSel.innerHTML = items.map(v => `<option value="${escHtml(v.id)}"${v.id===cur?' selected':''}>${escHtml(v.id)}</option>`).join('') || '<option value="">No voices</option>';
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('benchmark:tts-voices-fetched', { detail: { backend, voices: items } }));
|
||||
} catch(e) { toast('Fetch voices failed: '+e.message, 'error'); }
|
||||
finally { perfFetchBtn.disabled = false; }
|
||||
});
|
||||
@ -443,6 +444,7 @@ function renderPerfHistory() {
|
||||
return `<tr class="${ok?'':'perf-row-error'}">
|
||||
<td>${i+1}</td>
|
||||
<td>${escHtml(r.backend)}</td>
|
||||
<td><span class="bench-device ${typeof backendComputeDeviceClass === 'function' ? backendComputeDeviceClass(r.backend) : ''}" title="Inferred from the selected TTS backend metadata">${escHtml(typeof backendComputeDevice === 'function' ? backendComputeDevice(r.backend) : 'Unknown')}</span></td>
|
||||
<td>${escHtml(r.voice)}</td>
|
||||
<td>${ok ? r.latencyMs : '—'}</td>
|
||||
<td>${ok && r.audioDuration > 0 ? r.audioDuration.toFixed(2) : '—'}</td>
|
||||
|
||||
@ -103,6 +103,7 @@
|
||||
</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn-primary" id="perf-run-btn" type="button"><span class="mdi mdi-play"></span> Run benchmark</button>
|
||||
<button class="btn-secondary" id="perf-run-selected-btn" type="button" disabled title="Benchmark every checked voice in the Batch benchmark list below" aria-describedby="batch-benchmark-help"><span class="mdi mdi-playlist-play"></span> Run selected voices</button>
|
||||
<button class="btn-secondary" id="perf-clear-btn" type="button">Clear results</button>
|
||||
</div>
|
||||
<div id="perf-progress" class="note" style="display:none"></div>
|
||||
@ -124,7 +125,7 @@
|
||||
<table class="perf-table" id="perf-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th><th>Backend</th><th>Voice</th>
|
||||
<th>#</th><th>Backend</th><th>Device</th><th>Voice</th>
|
||||
<th>Latency (ms)</th><th>Audio (s)</th><th>RTF</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -148,17 +149,19 @@
|
||||
<option value="1" selected>1 run</option>
|
||||
<option value="3">3 runs</option>
|
||||
<option value="5">5 runs</option>
|
||||
<option value="10">10 runs</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="note" style="margin:4px 0 8px">Uses the sample text from the single-voice form above.</p>
|
||||
<p class="note" id="batch-benchmark-help" style="margin:4px 0 8px">Uses the sample text and Runs value from the single-voice form above. Check one voice, a few voices, or Select all, then run them together.</p>
|
||||
<div class="batch-voice-toolbar">
|
||||
<button class="btn-secondary btn-sm" id="batch-load-voices-btn" type="button"><span class="mdi mdi-reload"></span> Reload from backend</button>
|
||||
<button class="btn-secondary btn-sm" id="batch-select-all-btn" type="button">Select all</button>
|
||||
<button class="btn-secondary btn-sm" id="batch-select-none-btn" type="button">Deselect all</button>
|
||||
<span id="batch-selected-count" class="note"></span>
|
||||
<button class="btn-secondary btn-sm" id="batch-load-voices-btn" type="button" title="Load all voices exposed by the selected backend"><span class="mdi mdi-reload"></span> Reload from backend</button>
|
||||
<div class="batch-search-wrap"><span class="mdi mdi-magnify" aria-hidden="true"></span><input id="batch-voice-search" type="search" placeholder="Search voices..." autocomplete="off" spellcheck="false" aria-label="Search voices to benchmark"></div>
|
||||
<button class="btn-secondary btn-sm" id="batch-select-all-btn" type="button" title="Select every loaded voice for the batch benchmark">Select all</button>
|
||||
<button class="btn-secondary btn-sm" id="batch-select-none-btn" type="button" title="Clear the batch benchmark voice selection">Deselect all</button>
|
||||
<span id="batch-selected-count" class="note" aria-live="polite"></span>
|
||||
</div>
|
||||
<div id="batch-voice-list" class="batch-voice-list"></div>
|
||||
<div id="batch-voice-list" class="batch-voice-list" role="group" aria-label="Voices selected for batch benchmark" aria-describedby="batch-benchmark-help"></div>
|
||||
<div class="btn-row" style="margin-top:12px">
|
||||
<button class="btn-primary" id="batch-run-btn" type="button" disabled><span class="mdi mdi-play"></span> Run batch</button>
|
||||
<button class="btn-secondary" id="batch-stop-btn" type="button" disabled><span class="mdi mdi-stop"></span> Stop</button>
|
||||
@ -182,7 +185,7 @@
|
||||
<table class="perf-table" id="batch-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Voice</th><th>Avg latency</th><th>Best</th><th>Audio (s)</th><th>Avg RTF</th><th>Status</th>
|
||||
<th>Voice</th><th>Device</th><th>Avg latency</th><th>Best</th><th>Audio (s)</th><th>Avg RTF</th><th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="batch-tbody"></tbody>
|
||||
|
||||
@ -487,6 +487,13 @@ audio { width: 100%; }
|
||||
@media (max-width:1100px) { .settings-grid.compact { grid-template-columns:1fr; } .settings-behavior-grid { grid-template-columns:1fr; } }
|
||||
@media (max-width:900px) { .stt-settings-grid { grid-template-columns:1fr; } }
|
||||
|
||||
|
||||
/* Accessibility helpers */
|
||||
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
.skip-link { position: fixed; left: 12px; top: 12px; z-index: 6000; transform: translateY(-160%); background: var(--accent); color: #fff; padding: 9px 13px; border-radius: 7px; font-weight: 800; box-shadow: var(--shadow); }
|
||||
.skip-link:focus { transform: translateY(0); outline: 3px solid #fff; outline-offset: 2px; }
|
||||
button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible, .nav-item:focus-visible, .nav-tree-item:focus-visible { outline: 3px solid color-mix(in srgb, var(--accent) 78%, white); outline-offset: 2px; }
|
||||
|
||||
/* ── Toast ──────────────────────────────────────────────────────────────── */
|
||||
#toast { position: fixed; top: 50%; left: 50%; background: var(--surface); border: 2px solid var(--accent); border-left-width: 8px; color: var(--text); padding: 16px 20px; border-radius: 8px; font-size: 15px; font-weight: 700; opacity: 0; transform: translate(-50%, calc(-50% + 12px)) scale(.98); transition: opacity .18s, transform .18s; pointer-events: none; z-index: 5000; max-width: min(520px, calc(100vw - 40px)); min-width: min(360px, calc(100vw - 40px)); box-shadow: 0 20px 70px rgba(0,0,0,.35); text-align: center; }
|
||||
#toast.show { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
@ -2349,13 +2356,22 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.perf-history-del { cursor: pointer; color: var(--subtext); font-size: 11px; text-align: right; }
|
||||
.perf-history-del:hover { color: var(--red); }
|
||||
.batch-voice-toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.batch-voice-list { display: flex; flex-direction: column; gap: 0; max-height: 260px; overflow-y: auto; border: 1px solid var(--border); border-radius: 6px; background: var(--panel); }
|
||||
.batch-voice-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; cursor: pointer; font-size: 13px; border-bottom: 1px solid var(--border); transition: background .1s; }
|
||||
.batch-search-wrap { display: flex; align-items: center; gap: 7px; min-width: min(300px, 100%); flex: 1 1 260px; height: 34px; padding: 0 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--panel); color: var(--subtext); }
|
||||
.batch-search-wrap input { flex: 1; min-width: 0; border: 0; outline: 0; background: transparent; color: var(--text); font-size: 13px; }
|
||||
.batch-voice-list { display: flex; flex-direction: column; gap: 0; max-height: 320px; overflow-y: auto; border: 1px solid var(--border); border-radius: 7px; background: var(--panel); }
|
||||
.batch-voice-item { display: grid; grid-template-columns: auto auto minmax(0,1fr) auto auto; align-items: center; gap: 9px; padding: 7px 10px; cursor: pointer; font-size: 13px; border-bottom: 1px solid var(--border); transition: background .1s; }
|
||||
.batch-voice-item:last-child { border-bottom: none; }
|
||||
.batch-voice-item:hover { background: var(--hover); }
|
||||
.batch-voice-item.is-active .batch-voice-name { font-weight: 500; }
|
||||
.batch-voice-name { flex: 1; color: var(--text); }
|
||||
.batch-voice-tag { font-size: 10px; padding: 1px 6px; border-radius: 10px; background: rgba(var(--accent-rgb,99,102,241),.15); color: var(--accent); font-weight: 600; }
|
||||
.batch-voice-item.is-selected { background: rgba(37,99,235,.08); }
|
||||
.batch-voice-item.is-active .batch-voice-name { font-weight: 700; }
|
||||
.batch-voice-avatar { width: 28px; height: 28px; border-radius: 50%; object-fit: cover; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: #fff; font-size: 12px; font-weight: 700; }
|
||||
.batch-voice-main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
|
||||
.batch-voice-name, .batch-voice-id { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.batch-voice-name { color: var(--text); font-weight: 600; }
|
||||
.batch-voice-id { color: var(--subtext); font-size: 11px; font-family: monospace; }
|
||||
.batch-voice-stats { font-size: 11px; color: var(--subtext); font-variant-numeric: tabular-nums; white-space: nowrap; }
|
||||
.batch-voice-tag { font-size: 10px; padding: 1px 6px; border-radius: 10px; background: rgba(var(--accent-rgb,99,102,241),.15); color: var(--accent); font-weight: 600; white-space: nowrap; }
|
||||
@media (max-width: 700px) { .batch-voice-item { grid-template-columns: auto auto minmax(0,1fr); } .batch-voice-stats, .batch-voice-tag { grid-column: 3; justify-self: start; } }
|
||||
.batch-progress { margin-top: 12px; display: flex; flex-direction: column; gap: 6px; }
|
||||
.batch-progress-head { display: flex; justify-content: space-between; font-size: 12px; color: var(--subtext); }
|
||||
.batch-progress-track { height: 6px; border-radius: 3px; background: var(--border); overflow: hidden; }
|
||||
@ -2475,6 +2491,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.bench-device { display: inline-flex; align-items: center; padding: 2px 7px; border-radius: 999px; background: var(--panel); border: 1px solid var(--border); color: var(--subtext); font-size: 11px; font-weight: 700; }
|
||||
.bench-device.gpu { color: var(--green); background: rgba(22,163,74,.09); border-color: rgba(22,163,74,.28); }
|
||||
.bench-device.cpu { color: var(--yellow); background: rgba(217,119,6,.09); border-color: rgba(217,119,6,.28); }
|
||||
.bench-device.cloud { color: var(--accent); background: rgba(37,99,235,.08); border-color: rgba(37,99,235,.24); }
|
||||
.bench-inline-controls { display: flex; gap: 6px; min-width: min(620px, 100%); }
|
||||
.bench-inline-controls input, .bench-inline-controls select { min-width: 0; }
|
||||
.bench-turn-main .bench-turn-log { min-height: 440px; overflow-y: auto; padding: 16px; }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user