From ef5321cb1812107fc4da50e3b0826499dc19b4c3 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 28 Jun 2026 00:15:23 +0200 Subject: [PATCH] feat: lang/gender in batch benchmark list+results; sortable results table (v1.12.15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voice selection list now shows flag + gender symbol on each row. Batch results table adds Lang and Gender columns. All columns are sortable by clicking the header (↑↓ indicator); defaults to Factor descending. Sort logic handles strings (locale) and numbers uniformly. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 9 +++++ static/index.html | 4 +- static/js/benchmark.js | 64 ++++++++++++++++++++++++++---- static/sections/s-performance.html | 12 +++++- static/style.css | 8 +++- 5 files changed, 85 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13ff197..9b20510 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,15 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.12.15] — 2026-06-28 + +### Added +- **Lang + Gender in Benchmark batch results** — two new sortable columns after Voice. +- **Lang + Gender in voice selection list** — flag emoji and gender symbol shown on each voice row. +- **Sortable batch results table** — click any column header to sort; arrow indicator shows active sort direction. Defaults to Factor descending (fastest first). + +--- + ## [1.12.14] — 2026-06-27 ### Changed diff --git a/static/index.html b/static/index.html index 9c371bc..e21ebeb 100644 --- a/static/index.html +++ b/static/index.html @@ -26,7 +26,7 @@ - + @@ -336,7 +336,7 @@ - + diff --git a/static/js/benchmark.js b/static/js/benchmark.js index 8706a19..0236648 100644 --- a/static/js/benchmark.js +++ b/static/js/benchmark.js @@ -25,6 +25,8 @@ let batchResults = []; let batchVoices = []; let batchSelected = new Set(); + let batchSortField = 'factor'; + let batchSortDir = -1; // -1 = desc (fastest first) function populateBatchBackends() { if (!batchBackendSel) return; @@ -140,10 +142,17 @@ const checked = batchSelected.has(v.id); const isActive = activeIds.has(v.id); const stats = batchVoiceStats(v); + const meta = v.meta || libraryVoice(v.id) || {}; + const flag = meta.flag || ''; + const lang = meta.lang || meta.language || ''; + const genderMap = { M: '♂', F: '♀', N: '⚬' }; + const gender = genderMap[meta.gender] || ''; + const langGender = [flag || lang, gender].filter(Boolean).join(' '); return ``; @@ -219,27 +228,54 @@ buildVoiceList(detail.voices || []); }); + function batchSortValue(r, field) { + const factor = r.ok && r.avgRtf > 0 ? 1 / r.avgRtf : null; + switch (field) { + case 'voice': return (r.voice || '').toLowerCase(); + case 'lang': return (r.flag || r.lang || '').toLowerCase(); + case 'gender': return (r.gender || '').toLowerCase(); + case 'device': return typeof backendComputeDevice === 'function' ? backendComputeDevice(r.backend || '').toLowerCase() : ''; + case 'latency': return r.ok ? r.avgLatency : 999999; + case 'best': return r.ok ? r.minLatency : 999999; + case 'duration': return r.ok && r.avgAudio > 0 ? r.avgAudio : 999999; + case 'factor': return factor != null ? factor : -1; + case 'time': return r.ok ? r.avgLatency / 1000 : 999999; + case 'wpm': return r.ok && r.avgAudio > 0 && r.wordCount > 0 ? Math.round(r.wordCount / (r.avgAudio / 60)) : -1; + default: return 0; + } + } + 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; - // sort by Factor descending (higher = faster); Factor = 1/avgRtf - return (b.avgRtf > 0 ? 1/b.avgRtf : 0) - (a.avgRtf > 0 ? 1/a.avgRtf : 0); + const av = batchSortValue(a, batchSortField), bv = batchSortValue(b, batchSortField); + return typeof av === 'string' ? av.localeCompare(bv) * batchSortDir : (av - bv) * batchSortDir; }); const okCount = batchResults.filter(r => r.ok).length; if (batchResultsLabel) batchResultsLabel.textContent = `${okCount} / ${batchResults.length} voices — ${batchBackendSel.value}`; + + // Update header sort indicators + document.querySelectorAll('#batch-table th[data-sort]').forEach(th => { + th.classList.remove('perf-sort-asc', 'perf-sort-desc'); + if (th.dataset.sort === batchSortField) th.classList.add(batchSortDir === 1 ? 'perf-sort-asc' : 'perf-sort-desc'); + }); + batchTbody.innerHTML = sorted.map(r => { - const factor = r.ok && r.avgRtf > 0 ? 1 / r.avgRtf : null; - const timeSec = r.ok && r.avgLatency > 0 ? r.avgLatency / 1000 : null; - const wpm = r.ok && r.avgAudio > 0 && r.wordCount > 0 ? Math.round(r.wordCount / (r.avgAudio / 60)) : null; + const factor = r.ok && r.avgRtf > 0 ? 1 / r.avgRtf : null; + const timeSec = r.ok && r.avgLatency > 0 ? r.avgLatency / 1000 : null; + const wpm = r.ok && r.avgAudio > 0 && r.wordCount > 0 ? Math.round(r.wordCount / (r.avgAudio / 60)) : null; const factorCls = factor == null ? '' : factor >= 1 ? 'perf-good' : 'perf-slow'; - const trend = r.trend ? `${r.trend.label}` : ''; - const device = typeof backendComputeDevice === 'function' ? backendComputeDevice(r.backend || batchBackendSel.value) : 'Unknown'; + const trend = r.trend ? `${r.trend.label}` : ''; + const device = typeof backendComputeDevice === 'function' ? backendComputeDevice(r.backend || batchBackendSel.value) : 'Unknown'; const deviceCls = typeof backendComputeDeviceClass === 'function' ? backendComputeDeviceClass(r.backend || batchBackendSel.value) : ''; + const genderMap = { M: '♂', F: '♀', N: '⚬' }; return ` ${escHtml(r.voice)} + ${escHtml(r.flag || r.lang || '—')} + ${escHtml(genderMap[r.gender] || '—')} ${escHtml(device)} ${r.ok ? Math.round(r.avgLatency) + ' ms' : '—'} ${r.ok ? r.minLatency + ' ms' : '—'} @@ -252,6 +288,16 @@ }).join(''); } + // Wire sortable headers + document.querySelector('#batch-table')?.addEventListener('click', e => { + const th = e.target.closest('th[data-sort]'); + if (!th) return; + const field = th.dataset.sort; + if (field === batchSortField) batchSortDir *= -1; + else { batchSortField = field; batchSortDir = field === 'voice' || field === 'lang' || field === 'gender' ? 1 : -1; } + renderBatchResults(); + }); + async function runBatchBenchmark() { const topBackend = $('perf-backend-select')?.value || ''; if (topBackend && batchBackendSel && batchBackendSel.value !== topBackend) batchBackendSel.value = topBackend; @@ -287,7 +333,9 @@ batchProgCount.textContent = `${vi + 1} / ${selected.length}`; batchProgBar.style.width = `${Math.round((vi / selected.length) * 100)}%`; - const entry = { voice, backend, ok: false, avgLatency: 0, minLatency: 0, avgRtf: 0, avgAudio: 0, wordCount: text ? text.trim().split(/\s+/).length : 0, error: '' }; + const _vm = batchVoices.find(bv => bv.id === voice); + const _meta = _vm?.meta || libraryVoice(voice) || {}; + const entry = { voice, backend, ok: false, avgLatency: 0, minLatency: 0, avgRtf: 0, avgAudio: 0, wordCount: text ? text.trim().split(/\s+/).length : 0, lang: _meta.lang || _meta.language || '', flag: _meta.flag || '', gender: _meta.gender || '', error: '' }; const rowRunResults = []; for (let ri = 0; ri < runs; ri++) { diff --git a/static/sections/s-performance.html b/static/sections/s-performance.html index fab8195..df4d3b4 100644 --- a/static/sections/s-performance.html +++ b/static/sections/s-performance.html @@ -188,7 +188,17 @@ - + + + + + + + + + + + diff --git a/static/style.css b/static/style.css index 591c72a..e2ed8fd 100644 --- a/static/style.css +++ b/static/style.css @@ -2608,6 +2608,11 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .perf-table { width: 100%; border-collapse: collapse; font-size: 13px; } .perf-table th, .perf-table td { padding: 7px 10px; border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; } .perf-table th { color: var(--subtext); font-size: 11px; text-transform: uppercase; letter-spacing: .05em; } +.perf-th-sort { cursor: pointer; user-select: none; } +.perf-th-sort:hover { color: var(--text); } +.perf-th-sort::after { content: ' ⇅'; opacity: .35; font-size: 10px; } +.perf-th-sort.perf-sort-asc::after { content: ' ↑'; opacity: 1; color: var(--accent); } +.perf-th-sort.perf-sort-desc::after { content: ' ↓'; opacity: 1; color: var(--accent); } .perf-row-error td { color: var(--subtext); } .perf-ok { color: var(--green); font-weight: 600; } .perf-err { color: var(--red); } @@ -2644,7 +2649,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .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 { display: grid; grid-template-columns: auto auto minmax(0,1fr) auto 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-selected { background: rgba(37,99,235,.08); } @@ -2654,6 +2659,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .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-meta { font-size: 11px; color: var(--subtext); white-space: nowrap; } .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; } }
VoiceDeviceLatencyBestDurationFactorTimeWPMStatusVoiceLangGenderDeviceLatencyBestDurationFactorTimeWPMStatus