From c3b1cab75b4bb359db82769617df74513db67ebb Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 7 Jun 2026 15:41:04 +0200 Subject: [PATCH] Enhance benchmark history metadata --- static/app.js | 145 ++++++++++++++++++++++++++++++++++++- static/js/benchmark.js | 17 +++++ static/js/tts-preview.js | 143 +++++++++++++++++++++++++++++++++++- static/js/voice-library.js | 1 + static/style.css | 11 ++- 5 files changed, 309 insertions(+), 8 deletions(-) diff --git a/static/app.js b/static/app.js index 4e29128..b9bc01e 100644 --- a/static/app.js +++ b/static/app.js @@ -3390,6 +3390,7 @@ async function loadVoiceLibrary() { if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices); renderVoiceList(); updatePreviewVoiceMatchPanel(); + if (typeof renderPerfHistory === 'function') renderPerfHistory(); status(`Loaded ${_voices.length} voices`); } catch(e) { if (list) list.innerHTML = '
Failed to load voices
'; @@ -5899,6 +5900,7 @@ $('save-preview-btn').addEventListener('click', () => { const PERF_HISTORY_KEY = 'vcf-perf-history'; const PERF_HISTORY_MAX = 50; +const PERF_HISTORY_SORT = { key: 'ts', dir: 'desc' }; function perfHistoryLoad() { try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; } @@ -5926,6 +5928,114 @@ function perfSparklineSvg(rtfValues) { return ``; } +function perfHistoryVoiceLookup(voiceId) { + const list = Array.isArray(window._voices) && window._voices.length + ? window._voices + : (typeof _voices !== 'undefined' && Array.isArray(_voices) ? _voices : []); + return list.find(v => v && (v.id === voiceId || v.name === voiceId || v.voice_id === voiceId)) || null; +} + +function perfHistoryVoiceMeta(entry) { + const voice = entry?.voice || ''; + const saved = entry?.voiceMeta || {}; + const lib = perfHistoryVoiceLookup(voice) || {}; + const language = saved.lang || saved.language || entry?.lang || entry?.language || lib.lang || lib.language || ''; + const gender = String(saved.gender || entry?.gender || lib.gender || '').trim().toUpperCase().charAt(0); + return { + id: voice, + label: saved.label || saved.display_name || entry?.voiceLabel || lib.display_name || lib.name || voice, + lang: language, + gender: ['F', 'M', 'N'].includes(gender) ? gender : '', + flag: saved.flag || entry?.flag || lib.flag || '', + avatar: saved.avatar || entry?.avatar || lib.avatar || '', + hasPicture: Boolean(saved.has_picture ?? saved.hasPicture ?? entry?.hasPicture ?? lib.has_picture), + }; +} + +function perfHistoryExtraFields(voice, backend) { + const lib = perfHistoryVoiceLookup(voice) || {}; + return { + voiceMeta: { + label: lib.display_name || lib.name || voice, + lang: lib.lang || lib.language || '', + gender: lib.gender || '', + flag: lib.flag || '', + avatar: lib.avatar || '', + has_picture: !!lib.has_picture, + }, + device: typeof backendComputeDevice === 'function' ? backendComputeDevice(backend) : '', + }; +} + +function perfHistoryGenderLabel(gender) { + return ({F:'Female', M:'Male', N:'Diverse'}[gender] || ''); +} + +function perfHistoryDeviceLabel(entry) { + return entry?.device || (typeof backendComputeDevice === 'function' ? backendComputeDevice(entry?.backend || '') : '') || 'Unknown'; +} + +function perfHistoryDeviceHtml(entry) { + const label = perfHistoryDeviceLabel(entry); + const cls = typeof backendComputeDeviceClass === 'function' + ? backendComputeDeviceClass(entry?.backend || '') + : (label.toLowerCase().includes('gpu') ? 'gpu' : label.toLowerCase().includes('cpu') ? 'cpu' : ''); + return `${escHtml(label)}`; +} + +function perfHistoryAvatarHtml(entry) { + const meta = perfHistoryVoiceMeta(entry); + const title = meta.label || meta.id || 'Voice'; + if (meta.hasPicture) { + return ``; + } + const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta.avatar, 24) : null; + if (icon) return `${icon.replace(/vp-avatar/g, 'perf-history-avatar-icon')}`; + const color = typeof avatarColor === 'function' ? avatarColor(meta.lang || meta.id || title) : '#6b7280'; + const init = (title || '?').trim()[0]?.toUpperCase() || '?'; + return `${escHtml(init)}`; +} + + +function perfHistorySortValue(entry, key) { + switch (key) { + case 'backend': return String(entry.backend || '').toLowerCase(); + case 'language': return String(perfHistoryVoiceMeta(entry).lang || '').toLowerCase(); + case 'gender': return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender) || '').toLowerCase(); + case 'voice': return String(entry.voice || '').toLowerCase(); + case 'device': return String(perfHistoryDeviceLabel(entry) || '').toLowerCase(); + case 'avgLatencyMs': return Number(entry.avgLatencyMs); + case 'minLatencyMs': return Number(entry.minLatencyMs); + case 'avgRtf': return Number(entry.avgRtf); + case 'ts': + default: return Number(entry.ts); + } +} + +function perfHistoryCompare(a, b) { + const av = perfHistorySortValue(a, PERF_HISTORY_SORT.key); + const bv = perfHistorySortValue(b, PERF_HISTORY_SORT.key); + let result = 0; + if (typeof av === 'string' || typeof bv === 'string') { + result = String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' }); + } else { + const an = Number.isFinite(av) ? av : -Infinity; + const bn = Number.isFinite(bv) ? bv : -Infinity; + result = an === bn ? 0 : an - bn; + } + return PERF_HISTORY_SORT.dir === 'asc' ? result : -result; +} + +function perfHistoryHeadButton(key, label) { + const active = PERF_HISTORY_SORT.key === key; + const icon = active + ? (PERF_HISTORY_SORT.dir === 'asc' ? 'mdi-arrow-up' : 'mdi-arrow-down') + : 'mdi-swap-vertical'; + return ``; +} + function renderPerfHistory() { const histList = $('perf-history-list'); if (!histList) return; @@ -5933,23 +6043,38 @@ function renderPerfHistory() { const filterOn = filterEl?.checked; const curBack = $('perf-backend-select')?.value; const curVoice = $('perf-voice-select')?.value; - let entries = perfHistoryLoad().slice().reverse(); + let entries = perfHistoryLoad().slice(); if (filterOn && curBack) entries = entries.filter(e => e.backend === curBack && e.voice === curVoice); if (!entries.length) { histList.innerHTML = '
' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '
'; return; } + entries.sort((a, b) => perfHistoryCompare(a, b) || (Number(b.ts) - Number(a.ts))); const head = `
- Date / TimeBackendVoice - Avg latencyMinAvg RTF + ${perfHistoryHeadButton('ts', 'Date / Time')} + ${perfHistoryHeadButton('backend', 'Backend')} + Profile + ${perfHistoryHeadButton('language', 'Language')} + ${perfHistoryHeadButton('gender', 'Gender')} + ${perfHistoryHeadButton('voice', 'Voice')} + ${perfHistoryHeadButton('device', 'CPU / GPU')} + ${perfHistoryHeadButton('avgLatencyMs', 'Avg latency')} + ${perfHistoryHeadButton('minLatencyMs', 'Min')} + ${perfHistoryHeadButton('avgRtf', 'Avg RTF')} +
`; const rows = entries.map(e => { const dt = new Date(e.ts).toLocaleString([], {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}); const rtfCls = e.avgRtf < 1 ? 'perf-good' : 'perf-slow'; + const meta = perfHistoryVoiceMeta(e); return `
${escHtml(dt)} ${escHtml(e.backend)} + ${perfHistoryAvatarHtml(e)} + ${escHtml(meta.lang || '-')} + ${escHtml(perfHistoryGenderLabel(meta.gender) || '-')} ${escHtml(e.voice)} + ${perfHistoryDeviceHtml(e)} ${Math.round(e.avgLatencyMs)} ms ${Math.round(e.minLatencyMs)} ms ${e.avgRtf.toFixed(2)} @@ -5957,6 +6082,18 @@ function renderPerfHistory() {
`; }).join(''); histList.innerHTML = head + rows; + histList.querySelectorAll('.perf-history-sort').forEach(btn => { + btn.addEventListener('click', () => { + const key = btn.dataset.historySort || 'ts'; + if (PERF_HISTORY_SORT.key === key) { + PERF_HISTORY_SORT.dir = PERF_HISTORY_SORT.dir === 'asc' ? 'desc' : 'asc'; + } else { + PERF_HISTORY_SORT.key = key; + PERF_HISTORY_SORT.dir = key === 'backend' || key === 'voice' ? 'asc' : 'desc'; + } + renderPerfHistory(); + }); + }); histList.querySelectorAll('.perf-history-del').forEach(btn => { btn.addEventListener('click', () => { const ts = Number(btn.dataset.ts); @@ -6065,6 +6202,7 @@ function renderPerfHistory() { ts: Date.now(), backend: perfBackendSel.value, voice: perfVoiceSel.value, + ...perfHistoryExtraFields(perfVoiceSel.value, perfBackendSel.value), textLen: perfText.value.trim().length, avgLatencyMs: avg, minLatencyMs: minL, @@ -6313,6 +6451,7 @@ function renderPerfHistory() { } perfHistoryAdd({ ts: Date.now(), backend, voice, textLen: text.length, + ...perfHistoryExtraFields(voice, backend), avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency, maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)), avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true, diff --git a/static/js/benchmark.js b/static/js/benchmark.js index 0b42709..a39bd9c 100644 --- a/static/js/benchmark.js +++ b/static/js/benchmark.js @@ -78,6 +78,22 @@ return `${escHtml(init)}`; } + function batchHistoryExtraFields(voice, backend) { + const item = batchVoices.find(v => v.id === voice) || {}; + const meta = item.meta || libraryVoice(voice) || {}; + return { + voiceMeta: { + label: meta.display_name || meta.name || item.label || voice, + lang: meta.lang || meta.language || '', + gender: meta.gender || '', + flag: meta.flag || '', + avatar: meta.avatar || '', + has_picture: !!meta.has_picture, + }, + device: typeof backendComputeDevice === 'function' ? backendComputeDevice(backend) : '', + }; + } + 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)] @@ -305,6 +321,7 @@ } perfHistoryAdd({ ts: Date.now(), backend, voice, textLen: text.length, + ...batchHistoryExtraFields(voice, backend), avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency, maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)), avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true, diff --git a/static/js/tts-preview.js b/static/js/tts-preview.js index 88192f0..5070e7f 100644 --- a/static/js/tts-preview.js +++ b/static/js/tts-preview.js @@ -299,6 +299,7 @@ $('save-preview-btn').addEventListener('click', () => { const PERF_HISTORY_KEY = 'vcf-perf-history'; const PERF_HISTORY_MAX = 50; +const PERF_HISTORY_SORT = { key: 'ts', dir: 'desc' }; function perfHistoryLoad() { try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; } @@ -326,6 +327,114 @@ function perfSparklineSvg(rtfValues) { return ``; } +function perfHistoryVoiceLookup(voiceId) { + const list = Array.isArray(window._voices) && window._voices.length + ? window._voices + : (typeof _voices !== 'undefined' && Array.isArray(_voices) ? _voices : []); + return list.find(v => v && (v.id === voiceId || v.name === voiceId || v.voice_id === voiceId)) || null; +} + +function perfHistoryVoiceMeta(entry) { + const voice = entry?.voice || ''; + const saved = entry?.voiceMeta || {}; + const lib = perfHistoryVoiceLookup(voice) || {}; + const language = saved.lang || saved.language || entry?.lang || entry?.language || lib.lang || lib.language || ''; + const gender = String(saved.gender || entry?.gender || lib.gender || '').trim().toUpperCase().charAt(0); + return { + id: voice, + label: saved.label || saved.display_name || entry?.voiceLabel || lib.display_name || lib.name || voice, + lang: language, + gender: ['F', 'M', 'N'].includes(gender) ? gender : '', + flag: saved.flag || entry?.flag || lib.flag || '', + avatar: saved.avatar || entry?.avatar || lib.avatar || '', + hasPicture: Boolean(saved.has_picture ?? saved.hasPicture ?? entry?.hasPicture ?? lib.has_picture), + }; +} + +function perfHistoryExtraFields(voice, backend) { + const lib = perfHistoryVoiceLookup(voice) || {}; + return { + voiceMeta: { + label: lib.display_name || lib.name || voice, + lang: lib.lang || lib.language || '', + gender: lib.gender || '', + flag: lib.flag || '', + avatar: lib.avatar || '', + has_picture: !!lib.has_picture, + }, + device: typeof backendComputeDevice === 'function' ? backendComputeDevice(backend) : '', + }; +} + +function perfHistoryGenderLabel(gender) { + return ({F:'Female', M:'Male', N:'Diverse'}[gender] || ''); +} + +function perfHistoryDeviceLabel(entry) { + return entry?.device || (typeof backendComputeDevice === 'function' ? backendComputeDevice(entry?.backend || '') : '') || 'Unknown'; +} + +function perfHistoryDeviceHtml(entry) { + const label = perfHistoryDeviceLabel(entry); + const cls = typeof backendComputeDeviceClass === 'function' + ? backendComputeDeviceClass(entry?.backend || '') + : (label.toLowerCase().includes('gpu') ? 'gpu' : label.toLowerCase().includes('cpu') ? 'cpu' : ''); + return `${escHtml(label)}`; +} + +function perfHistoryAvatarHtml(entry) { + const meta = perfHistoryVoiceMeta(entry); + const title = meta.label || meta.id || 'Voice'; + if (meta.hasPicture) { + return ``; + } + const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta.avatar, 24) : null; + if (icon) return `${icon.replace(/vp-avatar/g, 'perf-history-avatar-icon')}`; + const color = typeof avatarColor === 'function' ? avatarColor(meta.lang || meta.id || title) : '#6b7280'; + const init = (title || '?').trim()[0]?.toUpperCase() || '?'; + return `${escHtml(init)}`; +} + + +function perfHistorySortValue(entry, key) { + switch (key) { + case 'backend': return String(entry.backend || '').toLowerCase(); + case 'language': return String(perfHistoryVoiceMeta(entry).lang || '').toLowerCase(); + case 'gender': return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender) || '').toLowerCase(); + case 'voice': return String(entry.voice || '').toLowerCase(); + case 'device': return String(perfHistoryDeviceLabel(entry) || '').toLowerCase(); + case 'avgLatencyMs': return Number(entry.avgLatencyMs); + case 'minLatencyMs': return Number(entry.minLatencyMs); + case 'avgRtf': return Number(entry.avgRtf); + case 'ts': + default: return Number(entry.ts); + } +} + +function perfHistoryCompare(a, b) { + const av = perfHistorySortValue(a, PERF_HISTORY_SORT.key); + const bv = perfHistorySortValue(b, PERF_HISTORY_SORT.key); + let result = 0; + if (typeof av === 'string' || typeof bv === 'string') { + result = String(av).localeCompare(String(bv), undefined, { numeric: true, sensitivity: 'base' }); + } else { + const an = Number.isFinite(av) ? av : -Infinity; + const bn = Number.isFinite(bv) ? bv : -Infinity; + result = an === bn ? 0 : an - bn; + } + return PERF_HISTORY_SORT.dir === 'asc' ? result : -result; +} + +function perfHistoryHeadButton(key, label) { + const active = PERF_HISTORY_SORT.key === key; + const icon = active + ? (PERF_HISTORY_SORT.dir === 'asc' ? 'mdi-arrow-up' : 'mdi-arrow-down') + : 'mdi-swap-vertical'; + return ``; +} + function renderPerfHistory() { const histList = $('perf-history-list'); if (!histList) return; @@ -333,23 +442,38 @@ function renderPerfHistory() { const filterOn = filterEl?.checked; const curBack = $('perf-backend-select')?.value; const curVoice = $('perf-voice-select')?.value; - let entries = perfHistoryLoad().slice().reverse(); + let entries = perfHistoryLoad().slice(); if (filterOn && curBack) entries = entries.filter(e => e.backend === curBack && e.voice === curVoice); if (!entries.length) { histList.innerHTML = '
' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '
'; return; } + entries.sort((a, b) => perfHistoryCompare(a, b) || (Number(b.ts) - Number(a.ts))); const head = `
- Date / TimeBackendVoice - Avg latencyMinAvg RTF + ${perfHistoryHeadButton('ts', 'Date / Time')} + ${perfHistoryHeadButton('backend', 'Backend')} + Profile + ${perfHistoryHeadButton('language', 'Language')} + ${perfHistoryHeadButton('gender', 'Gender')} + ${perfHistoryHeadButton('voice', 'Voice')} + ${perfHistoryHeadButton('device', 'CPU / GPU')} + ${perfHistoryHeadButton('avgLatencyMs', 'Avg latency')} + ${perfHistoryHeadButton('minLatencyMs', 'Min')} + ${perfHistoryHeadButton('avgRtf', 'Avg RTF')} +
`; const rows = entries.map(e => { const dt = new Date(e.ts).toLocaleString([], {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}); const rtfCls = e.avgRtf < 1 ? 'perf-good' : 'perf-slow'; + const meta = perfHistoryVoiceMeta(e); return `
${escHtml(dt)} ${escHtml(e.backend)} + ${perfHistoryAvatarHtml(e)} + ${escHtml(meta.lang || '-')} + ${escHtml(perfHistoryGenderLabel(meta.gender) || '-')} ${escHtml(e.voice)} + ${perfHistoryDeviceHtml(e)} ${Math.round(e.avgLatencyMs)} ms ${Math.round(e.minLatencyMs)} ms ${e.avgRtf.toFixed(2)} @@ -357,6 +481,18 @@ function renderPerfHistory() {
`; }).join(''); histList.innerHTML = head + rows; + histList.querySelectorAll('.perf-history-sort').forEach(btn => { + btn.addEventListener('click', () => { + const key = btn.dataset.historySort || 'ts'; + if (PERF_HISTORY_SORT.key === key) { + PERF_HISTORY_SORT.dir = PERF_HISTORY_SORT.dir === 'asc' ? 'desc' : 'asc'; + } else { + PERF_HISTORY_SORT.key = key; + PERF_HISTORY_SORT.dir = key === 'backend' || key === 'voice' ? 'asc' : 'desc'; + } + renderPerfHistory(); + }); + }); histList.querySelectorAll('.perf-history-del').forEach(btn => { btn.addEventListener('click', () => { const ts = Number(btn.dataset.ts); @@ -474,6 +610,7 @@ function renderPerfHistory() { ts: Date.now(), backend: perfBackendSel.value, voice: perfVoiceSel.value, + ...perfHistoryExtraFields(perfVoiceSel.value, perfBackendSel.value), textLen: perfText.value.trim().length, avgLatencyMs: avg, minLatencyMs: minL, diff --git a/static/js/voice-library.js b/static/js/voice-library.js index b62eac0..f202c08 100644 --- a/static/js/voice-library.js +++ b/static/js/voice-library.js @@ -475,6 +475,7 @@ async function loadVoiceLibrary() { if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices); renderVoiceList(); updatePreviewVoiceMatchPanel(); + if (typeof renderPerfHistory === 'function') renderPerfHistory(); status(`Loaded ${_voices.length} voices`); } catch(e) { if (list) list.innerHTML = '
Failed to load voices
'; diff --git a/static/style.css b/static/style.css index 10acda2..75d762a 100644 --- a/static/style.css +++ b/static/style.css @@ -2366,11 +2366,18 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .perf-sparkline { width: 120px; height: 32px; display: block; } .bench-history-toolbar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; } .bench-history-filter { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--subtext); cursor: pointer; } -.perf-history-list { display: flex; flex-direction: column; gap: 0; } +.perf-history-list { display: flex; flex-direction: column; gap: 0; overflow-x: auto; } .perf-history-empty { font-size: 13px; color: var(--subtext); padding: 16px 0; } -.perf-history-row { display: grid; grid-template-columns: 120px 1fr 1fr 80px 70px 70px 80px; gap: 0 10px; padding: 7px 4px; border-bottom: 1px solid var(--border); font-size: 12px; align-items: center; } +.perf-history-row { display: grid; grid-template-columns: 118px minmax(96px,.8fr) 34px 72px 72px minmax(190px,1.4fr) 92px 82px 68px 70px 24px; min-width: 1010px; gap: 0 10px; padding: 7px 4px; border-bottom: 1px solid var(--border); font-size: 12px; align-items: center; } .perf-history-row:last-child { border-bottom: none; } .perf-history-head { font-size: 11px; color: var(--subtext); text-transform: uppercase; letter-spacing: .05em; font-weight: 600; } +.perf-history-sort { display: inline-flex; align-items: center; gap: 3px; max-width: 100%; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-transform: inherit; letter-spacing: inherit; cursor: pointer; text-align: left; } +.perf-history-sort:hover, .perf-history-sort.active { color: var(--accent); } +.perf-history-sort .mdi { font-size: 13px; line-height: 1; } +.perf-history-avatar { width: 24px; height: 24px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; overflow: hidden; color: #fff; font-size: 11px; font-weight: 700; } +.perf-history-avatar img { width: 100%; height: 100%; object-fit: cover; display: block; } +.perf-history-avatar-icon { width: 24px !important; height: 24px !important; } +.perf-history-avatar-init { flex-shrink: 0; } .perf-history-ts { color: var(--subtext); font-size: 11px; } .perf-history-del { cursor: pointer; color: var(--subtext); font-size: 11px; text-align: right; } .perf-history-del:hover { color: var(--red); }