Enhance benchmark history metadata
This commit is contained in:
parent
52d3dcd135
commit
c3b1cab75b
145
static/app.js
145
static/app.js
@ -3390,6 +3390,7 @@ async function loadVoiceLibrary() {
|
|||||||
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
||||||
renderVoiceList();
|
renderVoiceList();
|
||||||
updatePreviewVoiceMatchPanel();
|
updatePreviewVoiceMatchPanel();
|
||||||
|
if (typeof renderPerfHistory === 'function') renderPerfHistory();
|
||||||
status(`Loaded ${_voices.length} voices`);
|
status(`Loaded ${_voices.length} voices`);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px">Failed to load voices</div>';
|
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px">Failed to load voices</div>';
|
||||||
@ -5899,6 +5900,7 @@ $('save-preview-btn').addEventListener('click', () => {
|
|||||||
|
|
||||||
const PERF_HISTORY_KEY = 'vcf-perf-history';
|
const PERF_HISTORY_KEY = 'vcf-perf-history';
|
||||||
const PERF_HISTORY_MAX = 50;
|
const PERF_HISTORY_MAX = 50;
|
||||||
|
const PERF_HISTORY_SORT = { key: 'ts', dir: 'desc' };
|
||||||
|
|
||||||
function perfHistoryLoad() {
|
function perfHistoryLoad() {
|
||||||
try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; }
|
try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; }
|
||||||
@ -5926,6 +5928,114 @@ function perfSparklineSvg(rtfValues) {
|
|||||||
return `<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`;
|
return `<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 `<span class="bench-device ${escHtml(cls)}">${escHtml(label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function perfHistoryAvatarHtml(entry) {
|
||||||
|
const meta = perfHistoryVoiceMeta(entry);
|
||||||
|
const title = meta.label || meta.id || 'Voice';
|
||||||
|
if (meta.hasPicture) {
|
||||||
|
return `<span class="perf-history-avatar" title="${escHtml(title)}"><img src="/api/voice/picture/${encodeURIComponent(meta.id)}" alt=""></span>`;
|
||||||
|
}
|
||||||
|
const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta.avatar, 24) : null;
|
||||||
|
if (icon) return `<span class="perf-history-avatar" title="${escHtml(title)}">${icon.replace(/vp-avatar/g, 'perf-history-avatar-icon')}</span>`;
|
||||||
|
const color = typeof avatarColor === 'function' ? avatarColor(meta.lang || meta.id || title) : '#6b7280';
|
||||||
|
const init = (title || '?').trim()[0]?.toUpperCase() || '?';
|
||||||
|
return `<span class="perf-history-avatar perf-history-avatar-init" style="background:${color}" title="${escHtml(title)}">${escHtml(init)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 `<button class="perf-history-sort${active ? ' active' : ''}" type="button" data-history-sort="${escHtml(key)}" aria-label="Sort by ${escHtml(label)}">
|
||||||
|
<span>${escHtml(label)}</span><span class="mdi ${icon}" aria-hidden="true"></span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderPerfHistory() {
|
function renderPerfHistory() {
|
||||||
const histList = $('perf-history-list');
|
const histList = $('perf-history-list');
|
||||||
if (!histList) return;
|
if (!histList) return;
|
||||||
@ -5933,23 +6043,38 @@ function renderPerfHistory() {
|
|||||||
const filterOn = filterEl?.checked;
|
const filterOn = filterEl?.checked;
|
||||||
const curBack = $('perf-backend-select')?.value;
|
const curBack = $('perf-backend-select')?.value;
|
||||||
const curVoice = $('perf-voice-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 (filterOn && curBack) entries = entries.filter(e => e.backend === curBack && e.voice === curVoice);
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
histList.innerHTML = '<div class="perf-history-empty">' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '</div>';
|
histList.innerHTML = '<div class="perf-history-empty">' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
entries.sort((a, b) => perfHistoryCompare(a, b) || (Number(b.ts) - Number(a.ts)));
|
||||||
const head = `<div class="perf-history-row perf-history-head">
|
const head = `<div class="perf-history-row perf-history-head">
|
||||||
<span>Date / Time</span><span>Backend</span><span>Voice</span>
|
<span>${perfHistoryHeadButton('ts', 'Date / Time')}</span>
|
||||||
<span>Avg latency</span><span>Min</span><span>Avg RTF</span><span></span>
|
<span>${perfHistoryHeadButton('backend', 'Backend')}</span>
|
||||||
|
<span>Profile</span>
|
||||||
|
<span>${perfHistoryHeadButton('language', 'Language')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('gender', 'Gender')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('voice', 'Voice')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('device', 'CPU / GPU')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('avgLatencyMs', 'Avg latency')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('minLatencyMs', 'Min')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('avgRtf', 'Avg RTF')}</span>
|
||||||
|
<span></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
const rows = entries.map(e => {
|
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 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 rtfCls = e.avgRtf < 1 ? 'perf-good' : 'perf-slow';
|
||||||
|
const meta = perfHistoryVoiceMeta(e);
|
||||||
return `<div class="perf-history-row">
|
return `<div class="perf-history-row">
|
||||||
<span class="perf-history-ts">${escHtml(dt)}</span>
|
<span class="perf-history-ts">${escHtml(dt)}</span>
|
||||||
<span>${escHtml(e.backend)}</span>
|
<span>${escHtml(e.backend)}</span>
|
||||||
|
<span>${perfHistoryAvatarHtml(e)}</span>
|
||||||
|
<span>${escHtml(meta.lang || '-')}</span>
|
||||||
|
<span>${escHtml(perfHistoryGenderLabel(meta.gender) || '-')}</span>
|
||||||
<span>${escHtml(e.voice)}</span>
|
<span>${escHtml(e.voice)}</span>
|
||||||
|
<span>${perfHistoryDeviceHtml(e)}</span>
|
||||||
<span>${Math.round(e.avgLatencyMs)} ms</span>
|
<span>${Math.round(e.avgLatencyMs)} ms</span>
|
||||||
<span>${Math.round(e.minLatencyMs)} ms</span>
|
<span>${Math.round(e.minLatencyMs)} ms</span>
|
||||||
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
|
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
|
||||||
@ -5957,6 +6082,18 @@ function renderPerfHistory() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
histList.innerHTML = head + rows;
|
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 => {
|
histList.querySelectorAll('.perf-history-del').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const ts = Number(btn.dataset.ts);
|
const ts = Number(btn.dataset.ts);
|
||||||
@ -6065,6 +6202,7 @@ function renderPerfHistory() {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
backend: perfBackendSel.value,
|
backend: perfBackendSel.value,
|
||||||
voice: perfVoiceSel.value,
|
voice: perfVoiceSel.value,
|
||||||
|
...perfHistoryExtraFields(perfVoiceSel.value, perfBackendSel.value),
|
||||||
textLen: perfText.value.trim().length,
|
textLen: perfText.value.trim().length,
|
||||||
avgLatencyMs: avg,
|
avgLatencyMs: avg,
|
||||||
minLatencyMs: minL,
|
minLatencyMs: minL,
|
||||||
@ -6313,6 +6451,7 @@ function renderPerfHistory() {
|
|||||||
}
|
}
|
||||||
perfHistoryAdd({
|
perfHistoryAdd({
|
||||||
ts: Date.now(), backend, voice, textLen: text.length,
|
ts: Date.now(), backend, voice, textLen: text.length,
|
||||||
|
...perfHistoryExtraFields(voice, backend),
|
||||||
avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency,
|
avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency,
|
||||||
maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)),
|
maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)),
|
||||||
avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true,
|
avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true,
|
||||||
|
|||||||
@ -78,6 +78,22 @@
|
|||||||
return `<span class="batch-voice-avatar batch-voice-avatar-init" style="background:${batchColor(meta.lang || v.id)}">${escHtml(init)}</span>`;
|
return `<span class="batch-voice-avatar batch-voice-avatar-init" style="background:${batchColor(meta.lang || v.id)}">${escHtml(init)}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
function batchSearchText(v) {
|
||||||
const meta = v.meta || {};
|
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)]
|
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({
|
perfHistoryAdd({
|
||||||
ts: Date.now(), backend, voice, textLen: text.length,
|
ts: Date.now(), backend, voice, textLen: text.length,
|
||||||
|
...batchHistoryExtraFields(voice, backend),
|
||||||
avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency,
|
avgLatencyMs: entry.avgLatency, minLatencyMs: entry.minLatency,
|
||||||
maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)),
|
maxLatencyMs: Math.max(...rowRunResults.map(r => r.lat)),
|
||||||
avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true,
|
avgRtf: entry.avgRtf, runCount: rowRunResults.length, allOk: true,
|
||||||
|
|||||||
@ -299,6 +299,7 @@ $('save-preview-btn').addEventListener('click', () => {
|
|||||||
|
|
||||||
const PERF_HISTORY_KEY = 'vcf-perf-history';
|
const PERF_HISTORY_KEY = 'vcf-perf-history';
|
||||||
const PERF_HISTORY_MAX = 50;
|
const PERF_HISTORY_MAX = 50;
|
||||||
|
const PERF_HISTORY_SORT = { key: 'ts', dir: 'desc' };
|
||||||
|
|
||||||
function perfHistoryLoad() {
|
function perfHistoryLoad() {
|
||||||
try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; }
|
try { return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY) || '[]'); } catch(_) { return []; }
|
||||||
@ -326,6 +327,114 @@ function perfSparklineSvg(rtfValues) {
|
|||||||
return `<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`;
|
return `<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 `<span class="bench-device ${escHtml(cls)}">${escHtml(label)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function perfHistoryAvatarHtml(entry) {
|
||||||
|
const meta = perfHistoryVoiceMeta(entry);
|
||||||
|
const title = meta.label || meta.id || 'Voice';
|
||||||
|
if (meta.hasPicture) {
|
||||||
|
return `<span class="perf-history-avatar" title="${escHtml(title)}"><img src="/api/voice/picture/${encodeURIComponent(meta.id)}" alt=""></span>`;
|
||||||
|
}
|
||||||
|
const icon = window.voiceAvatarIcon ? window.voiceAvatarIcon(meta.avatar, 24) : null;
|
||||||
|
if (icon) return `<span class="perf-history-avatar" title="${escHtml(title)}">${icon.replace(/vp-avatar/g, 'perf-history-avatar-icon')}</span>`;
|
||||||
|
const color = typeof avatarColor === 'function' ? avatarColor(meta.lang || meta.id || title) : '#6b7280';
|
||||||
|
const init = (title || '?').trim()[0]?.toUpperCase() || '?';
|
||||||
|
return `<span class="perf-history-avatar perf-history-avatar-init" style="background:${color}" title="${escHtml(title)}">${escHtml(init)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 `<button class="perf-history-sort${active ? ' active' : ''}" type="button" data-history-sort="${escHtml(key)}" aria-label="Sort by ${escHtml(label)}">
|
||||||
|
<span>${escHtml(label)}</span><span class="mdi ${icon}" aria-hidden="true"></span>
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderPerfHistory() {
|
function renderPerfHistory() {
|
||||||
const histList = $('perf-history-list');
|
const histList = $('perf-history-list');
|
||||||
if (!histList) return;
|
if (!histList) return;
|
||||||
@ -333,23 +442,38 @@ function renderPerfHistory() {
|
|||||||
const filterOn = filterEl?.checked;
|
const filterOn = filterEl?.checked;
|
||||||
const curBack = $('perf-backend-select')?.value;
|
const curBack = $('perf-backend-select')?.value;
|
||||||
const curVoice = $('perf-voice-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 (filterOn && curBack) entries = entries.filter(e => e.backend === curBack && e.voice === curVoice);
|
||||||
if (!entries.length) {
|
if (!entries.length) {
|
||||||
histList.innerHTML = '<div class="perf-history-empty">' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '</div>';
|
histList.innerHTML = '<div class="perf-history-empty">' + (filterOn ? 'No history for this backend/voice yet.' : 'No benchmark history yet. Run a benchmark above to start tracking.') + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
entries.sort((a, b) => perfHistoryCompare(a, b) || (Number(b.ts) - Number(a.ts)));
|
||||||
const head = `<div class="perf-history-row perf-history-head">
|
const head = `<div class="perf-history-row perf-history-head">
|
||||||
<span>Date / Time</span><span>Backend</span><span>Voice</span>
|
<span>${perfHistoryHeadButton('ts', 'Date / Time')}</span>
|
||||||
<span>Avg latency</span><span>Min</span><span>Avg RTF</span><span></span>
|
<span>${perfHistoryHeadButton('backend', 'Backend')}</span>
|
||||||
|
<span>Profile</span>
|
||||||
|
<span>${perfHistoryHeadButton('language', 'Language')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('gender', 'Gender')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('voice', 'Voice')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('device', 'CPU / GPU')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('avgLatencyMs', 'Avg latency')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('minLatencyMs', 'Min')}</span>
|
||||||
|
<span>${perfHistoryHeadButton('avgRtf', 'Avg RTF')}</span>
|
||||||
|
<span></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
const rows = entries.map(e => {
|
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 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 rtfCls = e.avgRtf < 1 ? 'perf-good' : 'perf-slow';
|
||||||
|
const meta = perfHistoryVoiceMeta(e);
|
||||||
return `<div class="perf-history-row">
|
return `<div class="perf-history-row">
|
||||||
<span class="perf-history-ts">${escHtml(dt)}</span>
|
<span class="perf-history-ts">${escHtml(dt)}</span>
|
||||||
<span>${escHtml(e.backend)}</span>
|
<span>${escHtml(e.backend)}</span>
|
||||||
|
<span>${perfHistoryAvatarHtml(e)}</span>
|
||||||
|
<span>${escHtml(meta.lang || '-')}</span>
|
||||||
|
<span>${escHtml(perfHistoryGenderLabel(meta.gender) || '-')}</span>
|
||||||
<span>${escHtml(e.voice)}</span>
|
<span>${escHtml(e.voice)}</span>
|
||||||
|
<span>${perfHistoryDeviceHtml(e)}</span>
|
||||||
<span>${Math.round(e.avgLatencyMs)} ms</span>
|
<span>${Math.round(e.avgLatencyMs)} ms</span>
|
||||||
<span>${Math.round(e.minLatencyMs)} ms</span>
|
<span>${Math.round(e.minLatencyMs)} ms</span>
|
||||||
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
|
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
|
||||||
@ -357,6 +481,18 @@ function renderPerfHistory() {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
histList.innerHTML = head + rows;
|
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 => {
|
histList.querySelectorAll('.perf-history-del').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const ts = Number(btn.dataset.ts);
|
const ts = Number(btn.dataset.ts);
|
||||||
@ -474,6 +610,7 @@ function renderPerfHistory() {
|
|||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
backend: perfBackendSel.value,
|
backend: perfBackendSel.value,
|
||||||
voice: perfVoiceSel.value,
|
voice: perfVoiceSel.value,
|
||||||
|
...perfHistoryExtraFields(perfVoiceSel.value, perfBackendSel.value),
|
||||||
textLen: perfText.value.trim().length,
|
textLen: perfText.value.trim().length,
|
||||||
avgLatencyMs: avg,
|
avgLatencyMs: avg,
|
||||||
minLatencyMs: minL,
|
minLatencyMs: minL,
|
||||||
|
|||||||
@ -475,6 +475,7 @@ async function loadVoiceLibrary() {
|
|||||||
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
if (typeof window.updateVoiceTree === 'function') window.updateVoiceTree(_voices);
|
||||||
renderVoiceList();
|
renderVoiceList();
|
||||||
updatePreviewVoiceMatchPanel();
|
updatePreviewVoiceMatchPanel();
|
||||||
|
if (typeof renderPerfHistory === 'function') renderPerfHistory();
|
||||||
status(`Loaded ${_voices.length} voices`);
|
status(`Loaded ${_voices.length} voices`);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px">Failed to load voices</div>';
|
if (list) list.innerHTML = '<div style="color:var(--red);padding:8px">Failed to load voices</div>';
|
||||||
|
|||||||
@ -2366,11 +2366,18 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
|||||||
.perf-sparkline { width: 120px; height: 32px; display: block; }
|
.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-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; }
|
.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-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-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-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-ts { color: var(--subtext); font-size: 11px; }
|
||||||
.perf-history-del { cursor: pointer; color: var(--subtext); font-size: 11px; text-align: right; }
|
.perf-history-del { cursor: pointer; color: var(--subtext); font-size: 11px; text-align: right; }
|
||||||
.perf-history-del:hover { color: var(--red); }
|
.perf-history-del:hover { color: var(--red); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user