feat: reorder columns, add Duration column, benchmark sentence presets (v1.12.12)
Column order now: Img·Play·Name·Lang·Gender·Length·Duration·WPM·Speed· Seed·dBFS·Type·Source·Rating·Tags·Note·Active. Duration shows synthesised audio length (b.audio_sec), sortable. Preset dropdown offers 4 benchmark sentences (DE/EN narrative + DE/EN tongue-twister) and a reset option. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ca3ce3adcb
commit
0606db101a
11
CHANGELOG.md
11
CHANGELOG.md
@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
|
||||
---
|
||||
|
||||
## [1.12.12] — 2026-06-27
|
||||
|
||||
### Added
|
||||
- **Duration column** in the voice table — shows the length of the synthesised benchmark audio (e.g. `11.4s`), sortable. Distinct from Length (original clip) and WPM (speaking rate).
|
||||
- **Benchmark sentence presets** dropdown — four ready-made sentences (DE narrative, EN narrative, DE pangram, EN tongue-twister) plus a Reset option. Selecting a preset loads it instantly without overwriting anything else.
|
||||
|
||||
### Changed
|
||||
- **Column order** rearranged to: Img · Play · Name · Lang · Gender · Length · Duration · WPM · Speed · Seed · dBFS · Type · Source · Rating · Tags · Note · Active.
|
||||
|
||||
---
|
||||
|
||||
## [1.12.11] — 2026-06-27
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.11">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.12.12">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -336,7 +336,7 @@
|
||||
<script src="/static/vendor/wavesurfer-regions.min.js"></script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.12.11"></script>
|
||||
<script src="/static/loader.js?v=1.12.12"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -45,6 +45,14 @@ const BENCHMARK_SAMPLE_STORAGE_KEY = 'vcf-benchmark-sample-text';
|
||||
const _libraryFilters = {text:'', lang:'', sex:'', type:'', rating:''};
|
||||
const DEFAULT_BENCHMARK_SAMPLE_TEXT = 'Hello, how are you today? Please read this sample clearly for a fair voice benchmark.';
|
||||
|
||||
const BENCHMARK_PRESETS = {
|
||||
de: 'Die Welt ist voller Geschichten, die darauf warten, erzählt zu werden — von mutigen Helden und stillen Träumern.',
|
||||
en: 'The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.',
|
||||
de2: 'Victor jagt zwölf Boxkämpfer quer über den großen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.',
|
||||
en2: 'She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.',
|
||||
reset: DEFAULT_BENCHMARK_SAMPLE_TEXT,
|
||||
};
|
||||
|
||||
function benchmarkSampleText() {
|
||||
const el = $('benchmark-sample-text');
|
||||
return (el && el.value.trim()) || DEFAULT_BENCHMARK_SAMPLE_TEXT;
|
||||
@ -63,6 +71,17 @@ function initBenchmarkSampleControls() {
|
||||
localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, sample.value);
|
||||
status('Benchmark sample sentence reset');
|
||||
});
|
||||
const presetSel = $('benchmark-preset-select');
|
||||
if (presetSel) {
|
||||
presetSel.addEventListener('change', () => {
|
||||
const key = presetSel.value;
|
||||
if (!key || !BENCHMARK_PRESETS[key]) { presetSel.value = ''; return; }
|
||||
sample.value = BENCHMARK_PRESETS[key];
|
||||
localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY, sample.value);
|
||||
presetSel.value = '';
|
||||
status('Benchmark sample sentence loaded');
|
||||
});
|
||||
}
|
||||
$('benchmark-use-preview-btn')?.addEventListener('click', () => {
|
||||
const text = $('preview-text-area')?.value.trim();
|
||||
if (!text) { toast('Preview text is empty', 'error'); return; }
|
||||
@ -98,6 +117,7 @@ function getSortValue(v, field) {
|
||||
case 'duration': return v.duration || 0;
|
||||
case 'dbfs': return voiceDbfs(v) ?? -999;
|
||||
case 'benchmark': { const b = voiceBenchmark(v); return b && b.speed != null ? -Number(b.speed) : 999; }
|
||||
case 'bench_audio': return voiceBenchmarkAudioSec(v) ?? 999;
|
||||
case 'wpm': return voiceWpm(v) ?? -1;
|
||||
case 'transcript': return (v.transcript || '').toLowerCase();
|
||||
case 'note': return (v.note || '').toLowerCase();
|
||||
@ -377,6 +397,16 @@ function benchmarkClass(v) {
|
||||
return elapsed != null && elapsed <= 4 ? 'bench-ok' : 'bench-warn';
|
||||
}
|
||||
|
||||
function voiceBenchmarkAudioSec(v) {
|
||||
const b = voiceBenchmark(v);
|
||||
return b && b.ok && b.audio_sec != null ? Number(b.audio_sec) : null;
|
||||
}
|
||||
|
||||
function fmtBenchmarkAudio(v) {
|
||||
const sec = voiceBenchmarkAudioSec(v);
|
||||
return sec != null ? sec.toFixed(1) + 's' : '-';
|
||||
}
|
||||
|
||||
function voiceWpm(v) {
|
||||
const b = voiceBenchmark(v);
|
||||
if (!b || !b.ok || !b.audio_sec || !b.text) return null;
|
||||
@ -836,11 +866,17 @@ async function runVoiceBenchmarkBatch() {
|
||||
// Live-update the row cells so results appear immediately
|
||||
if (row) {
|
||||
const benchCell = row.querySelector('.vl-tbl-bench');
|
||||
const durCell = row.querySelector('.vl-tbl-dur');
|
||||
const wpmCell = row.querySelector('.vl-tbl-wpm');
|
||||
if (benchCell) {
|
||||
const bt = fmtBenchmark(voice), bc = benchmarkClass(voice), btitle = benchmarkTitle(voice);
|
||||
benchCell.innerHTML = bt !== '-' ? `<span class="vl-bench-chip ${bc}" title="${escHtml(btitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(bt)}</span>` : '-';
|
||||
}
|
||||
if (durCell) {
|
||||
const sec = voiceBenchmarkAudioSec(voice);
|
||||
durCell.textContent = fmtBenchmarkAudio(voice);
|
||||
durCell.title = sec != null ? `${fmtBenchmarkAudio(voice)} of synthesised audio for the benchmark sentence` : '';
|
||||
}
|
||||
if (wpmCell) {
|
||||
const wpm = voiceWpm(voice);
|
||||
wpmCell.textContent = fmtWpm(voice);
|
||||
@ -1978,6 +2014,7 @@ function makeVoiceRow(v) {
|
||||
<div class="vl-tbl-bench">${benchText !== '-' ? `<span class="vl-bench-chip ${benchCls}" title="${escHtml(benchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(benchText)}</span>` : '-'}</div>
|
||||
<div class="vl-tbl-dbfs" title="${dbTitle}">${dbfs} dB</div>
|
||||
<div class="vl-tbl-len">${fmtDuration(v.duration)}</div>
|
||||
<div class="vl-tbl-dur" title="${escHtml(voiceBenchmarkAudioSec(v) != null ? `${fmtBenchmarkAudio(v)} of synthesised audio for the benchmark sentence` : 'Benchmark this voice to see output duration')}">${escHtml(fmtBenchmarkAudio(v))}</div>
|
||||
<div class="vl-tbl-wpm" title="${escHtml(voiceWpm(v) != null ? `${voiceWpm(v)} words per minute — how fast this voice speaks. 130–180 wpm is natural for long listening.` : 'Benchmark this voice to see speaking rate')}">${escHtml(fmtWpm(v))}</div>
|
||||
<div class="vl-tbl-rating">
|
||||
${window._voiceTableEditMode
|
||||
@ -2945,11 +2982,17 @@ function makeVoiceRow(v) {
|
||||
const tblRow = document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);
|
||||
if (tblRow) {
|
||||
const tblBench = tblRow.querySelector('.vl-tbl-bench');
|
||||
const tblDur = tblRow.querySelector('.vl-tbl-dur');
|
||||
const tblWpm = tblRow.querySelector('.vl-tbl-wpm');
|
||||
if (tblBench) {
|
||||
const bt = fmtBenchmark(v), bc = benchmarkClass(v), btitle = benchmarkTitle(v);
|
||||
tblBench.innerHTML = bt !== '-' ? `<span class="vl-bench-chip ${bc}" title="${escHtml(btitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(bt)}</span>` : '-';
|
||||
}
|
||||
if (tblDur) {
|
||||
const sec = voiceBenchmarkAudioSec(v);
|
||||
tblDur.textContent = fmtBenchmarkAudio(v);
|
||||
tblDur.title = sec != null ? `${fmtBenchmarkAudio(v)} of synthesised audio for the benchmark sentence` : '';
|
||||
}
|
||||
if (tblWpm) {
|
||||
const wpm = voiceWpm(v);
|
||||
tblWpm.textContent = fmtWpm(v);
|
||||
|
||||
@ -108,16 +108,17 @@
|
||||
<div class="vl-th-sortable" data-sort="id">Name <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="flag">Lang <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="gender">Gender <span class="mdi mdi-sort"></span></div>
|
||||
<div>Type</div>
|
||||
<div class="vl-th-sortable" data-sort="benchmark">Speed <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Synthesis speed: seconds of audio produced per second of compute (higher = faster GPU). Shown as: render_time · multiplier×"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="dbfs">dBFS <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="duration">Length <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="bench_audio">Duration <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Length of the synthesised benchmark audio — how many seconds of speech the engine produced for the test sentence."></span></div>
|
||||
<div class="vl-th-sortable" data-sort="wpm">WPM <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Words per minute the voice speaks. 130–180 wpm is natural for audiobooks; lower = slower, more deliberate pace."></span></div>
|
||||
<div class="vl-th-sortable" data-sort="rating">Rating <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="source">Source <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="benchmark">Speed <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Synthesis speed: shown as render_time · multiplier×. The multiplier is seconds of audio produced per second of compute — higher means a faster GPU / engine."></span></div>
|
||||
<div class="vl-th-sortable" data-sort="seed">Seed <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="note">Note <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="dbfs">dBFS <span class="mdi mdi-sort"></span></div>
|
||||
<div>Type</div>
|
||||
<div class="vl-th-sortable" data-sort="source">Source <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="rating">Rating <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="tag">Tags <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="note">Note <span class="mdi mdi-sort"></span></div>
|
||||
<div class="vl-th-sortable" data-sort="enabled">Active <span class="mdi mdi-sort"></span></div>
|
||||
</div>
|
||||
<div id="voice-list" tabindex="0" role="region" aria-label="Voice library"></div>
|
||||
@ -174,7 +175,14 @@
|
||||
<select id="library-tts-backend-select"><option value="">Checking backends...</option></select>
|
||||
<input type="number" id="library-target-db" value="-20" min="-60" max="-1" step="0.5">
|
||||
<button id="normalize-volume-btn">Normalize volume</button>
|
||||
<button id="benchmark-reset-sample-btn">Reset sample</button>
|
||||
<select id="benchmark-preset-select" title="Load a benchmark sentence preset">
|
||||
<option value="">Sentence presets…</option>
|
||||
<option value="de">DE — Die Welt ist voller Geschichten…</option>
|
||||
<option value="en">EN — The old lighthouse stood firm…</option>
|
||||
<option value="de2">DE — Der schnelle braune Fuchs…</option>
|
||||
<option value="en2">EN — She sells seashells…</option>
|
||||
<option value="reset">↺ Reset to default</option>
|
||||
</select>
|
||||
<button id="benchmark-use-preview-btn">Use preview text</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1516,7 +1516,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.vl-dbfs { display: none; }
|
||||
|
||||
/* Table View mode */
|
||||
.vl-tbl-lang, .vl-tbl-gender, .vl-tbl-type, .vl-tbl-bench, .vl-tbl-dbfs, .vl-tbl-len, .vl-tbl-wpm, .vl-tbl-rating, .vl-tbl-source, .vl-tbl-seed, .vl-tbl-note, .vl-tbl-tag, .vl-tbl-active { display: none; }
|
||||
.vl-tbl-lang, .vl-tbl-gender, .vl-tbl-type, .vl-tbl-bench, .vl-tbl-dbfs, .vl-tbl-len, .vl-tbl-dur, .vl-tbl-wpm, .vl-tbl-rating, .vl-tbl-source, .vl-tbl-seed, .vl-tbl-note, .vl-tbl-tag, .vl-tbl-active { display: none; }
|
||||
.vl-table-header { display: none; }
|
||||
|
||||
.voices-workbench.table-view .voices-list-pane { width: 100% !important; max-width: 100%; }
|
||||
@ -1524,7 +1524,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
|
||||
.voices-workbench.table-view .vl-table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 32px 60px minmax(140px, 1.2fr) 60px 70px 60px 80px 70px 60px 55px 90px 70px 60px minmax(120px, 1fr) minmax(80px, 1fr) 50px;
|
||||
grid-template-columns: 24px 32px 60px minmax(140px, 1.2fr) 60px 70px 55px 55px 55px 80px 55px 70px 60px 90px 70px minmax(80px, 1fr) minmax(120px, 1fr) 50px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 8px 10px;
|
||||
@ -1543,7 +1543,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
|
||||
.voices-workbench.table-view .vl-compact {
|
||||
display: grid;
|
||||
grid-template-columns: 24px 32px 60px minmax(140px, 1.2fr) 60px 70px 60px 80px 70px 60px 55px 90px 70px 60px minmax(120px, 1fr) minmax(80px, 1fr) 50px;
|
||||
grid-template-columns: 24px 32px 60px minmax(140px, 1.2fr) 60px 70px 55px 55px 55px 80px 55px 70px 60px 90px 70px minmax(80px, 1fr) minmax(120px, 1fr) 50px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
@ -1556,18 +1556,19 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
|
||||
.voices-workbench.table-view .vl-name-input { order: 3; display: block; font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); }
|
||||
.voices-workbench.table-view .vl-tbl-lang { display: block; order: 4; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-gender { display: block; order: 5; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-type { display: block; order: 6; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-bench { display: block; order: 7; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-dbfs { display: block; order: 8; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-len { display: block; order: 9; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-wpm { display: block; order: 10; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-rating { display: flex; order: 11; font-size: 14px; color: var(--subtext); }
|
||||
.voices-workbench.table-view .vl-tbl-len { display: block; order: 6; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-dur { display: block; order: 7; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-wpm { display: block; order: 8; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-bench { display: block; order: 9; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-seed { display: block; order: 10; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-dbfs { display: block; order: 11; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-type { display: block; order: 12; font-size: 12px; }
|
||||
.voices-workbench.table-view .vl-tbl-source { display: block; order: 13; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.voices-workbench.table-view .vl-tbl-rating { display: flex; order: 14; font-size: 14px; color: var(--subtext); }
|
||||
.voices-workbench.table-view .vl-tbl-rating .star.on { color: #f59e0b; }
|
||||
.voices-workbench.table-view .vl-tbl-source { display: block; order: 12; font-size: 12px; overflow: hidden; text-overflow: ellipsis; }
|
||||
.voices-workbench.table-view .vl-tbl-seed { display: block; order: 13; font-size: 12px; font-family: monospace; }
|
||||
.voices-workbench.table-view .vl-tbl-note { display: block; order: 14; font-size: 12px; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
|
||||
.voices-workbench.table-view .vl-tbl-tag { display: block; order: 15; font-size: 12px; color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
|
||||
.voices-workbench.table-view .vl-tbl-active { display: block; order: 16; font-size: 16px; text-align: center; }
|
||||
.voices-workbench.table-view .vl-tbl-note { display: block; order: 16; font-size: 12px; color: var(--subtext); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-style: italic; }
|
||||
.voices-workbench.table-view .vl-tbl-active { display: block; order: 17; font-size: 16px; text-align: center; }
|
||||
|
||||
.voices-workbench.table-edit-mode .vl-compact {
|
||||
background: rgba(37,99,235,0.03);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user