fix: voice inspector TDZ crash + batch ops respect selection (v1.9.7)

- voice-inspector: move `curGender` const before its first use — it was
  declared at line 86 but referenced at line 61, causing a ReferenceError
  (temporal dead zone) that silently aborted selectVoice() on every click
- Calc dB, Precompute, Batch Seeds now filter to checked voices when a
  bulk selection is active, matching Benchmark's existing behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-27 10:06:06 +02:00
parent 6b52a346d5
commit 217d92b91a
6 changed files with 30 additions and 14 deletions

View File

@ -9,6 +9,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.9.7] — 2026-06-27
### Fixed
- **Voice inspector broken**: `curGender` was referenced before its `const` declaration inside `selectVoice()`, causing a temporal dead zone ReferenceError that silently prevented the inspector from opening. Clicking a voice now correctly opens the detail panel again.
- **Batch operations ignore selection**: Calc dB, Precompute, and Batch Seeds now operate only on the checked (selected) voices when a selection is active, matching the existing behaviour of Benchmark. Previously all three always ran on all active / visible voices regardless of selection.
---
## [1.9.6] — 2026-06-26 ## [1.9.6] — 2026-06-26
### Fixed ### Fixed

View File

@ -1 +1 @@
1.9.6 1.9.7

View File

@ -26,7 +26,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── --> <!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css"> <link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.9.6"> <link rel="stylesheet" href="/static/style.css?v=1.9.7">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -308,7 +308,7 @@
<script src="/static/vendor/wavesurfer-regions.min.js"></script> <script src="/static/vendor/wavesurfer-regions.min.js"></script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton --> <!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.9.6"></script> <script src="/static/loader.js?v=1.9.7"></script>
</body> </body>
</html> </html>

View File

@ -431,10 +431,13 @@ function attachSeedFinder(voiceId, body) {
// writes to the same cache the per-voice Seed Finder reads, so opening any voice // writes to the same cache the per-voice Seed Finder reads, so opening any voice
// later shows its samples instantly. // later shows its samples instantly.
function seedFinderBatchAll() { function seedFinderBatchAll() {
const ids = (typeof activeVoiceIds === 'function') const _sfUseSelected = typeof _bulkSelected !== 'undefined' && _bulkSelected.size > 0;
const ids = _sfUseSelected
? [..._bulkSelected]
: (typeof activeVoiceIds === 'function')
? activeVoiceIds() ? activeVoiceIds()
: (window._voices || []).filter(v => v.enabled !== false).map(v => v.id); : (window._voices || []).filter(v => v.enabled !== false).map(v => v.id);
if (!ids.length) { if (typeof toast === 'function') toast('No active voices', 'error'); return; } if (!ids.length) { if (typeof toast === 'function') toast('No voices selected', 'error'); return; }
// In-app config dialog (no browser prompts): pick the seed range, then Start. // In-app config dialog (no browser prompts): pick the seed range, then Start.
document.getElementById('seedbatch-overlay')?.remove(); document.getElementById('seedbatch-overlay')?.remove();
@ -442,7 +445,7 @@ function seedFinderBatchAll() {
ov.className = 'audiobook-overlay'; ov.id = 'seedbatch-overlay'; ov.className = 'audiobook-overlay'; ov.id = 'seedbatch-overlay';
ov.innerHTML = `<div class="audiobook-box"> ov.innerHTML = `<div class="audiobook-box">
<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div> <div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
<div class="audiobook-msg">Generate &amp; cache Seed Finder samples for all <b>${ids.length}</b> active voices. Already-cached seeds are skipped, so it's resumable; you can cancel anytime.</div> <div class="audiobook-msg">Generate &amp; cache Seed Finder samples for <b>${ids.length}</b> ${_sfUseSelected ? 'selected' : 'active'} voice${ids.length !== 1 ? 's' : ''}. Already-cached seeds are skipped, so it's resumable; you can cancel anytime.</div>
<div class="seed-finder-row seed-finder-params" style="margin:12px 0"> <div class="seed-finder-row seed-finder-params" style="margin:12px 0">
<div class="seed-finder-field"> <div class="seed-finder-field">
<label>Seeds: from</label> <label>Seeds: from</label>

View File

@ -45,6 +45,7 @@ function selectVoice(wrap) {
const langCode = v.lang || wrap.dataset.lang || ''; const langCode = v.lang || wrap.dataset.lang || '';
const flagCc = v.flag || wrap.dataset.flagCc || langCode; const flagCc = v.flag || wrap.dataset.flagCc || langCode;
const rating = v.rating || 0; const rating = v.rating || 0;
const curGender = v.gender || 'N';
const nameParts = voiceId.split('_'); const nameParts = voiceId.split('_');
// Prefer an explicit display name (e.g. the character name for Rehearser voices); // Prefer an explicit display name (e.g. the character name for Rehearser voices);
@ -83,7 +84,6 @@ function selectVoice(wrap) {
const _gM = {F:'♀', M:'♂', N:'⚥', '':'?'}; const _gM = {F:'♀', M:'♂', N:'⚥', '':'?'};
const _gL = {F:'Female', M:'Male', N:'Diverse', '':'—'}; const _gL = {F:'Female', M:'Male', N:'Diverse', '':'—'};
const _gC = {F:'g-f', M:'g-m', N:'g-n', '':'g-n'}; const _gC = {F:'g-f', M:'g-m', N:'g-n', '':'g-n'};
const curGender = v.gender || 'N';
const genderLabelHtml = `${_gM[curGender]||'?'} ${_gL[curGender]||'—'}`; const genderLabelHtml = `${_gM[curGender]||'?'} ${_gL[curGender]||'—'}`;
inspector.innerHTML = ` inspector.innerHTML = `

View File

@ -417,7 +417,9 @@ async function clientVoiceLoudness(v) {
} }
async function clientCalculateVoiceDb() { async function clientCalculateVoiceDb() {
const voices = visibleLibraryVoices(); const voices = (_bulkSelected && _bulkSelected.size > 0)
? (_voices || []).filter(v => _bulkSelected.has(v.id))
: visibleLibraryVoices();
const errors = []; const errors = [];
let calculated = 0; let calculated = 0;
const stats = {startedAt: Date.now(), ok: 0, slow: 0, errors: 0, middleLabel: 'Skipped'}; const stats = {startedAt: Date.now(), ok: 0, slow: 0, errors: 0, middleLabel: 'Skipped'};
@ -622,7 +624,8 @@ function libraryTargetDb() {
$('calculate-db-btn').addEventListener('click', async () => { $('calculate-db-btn').addEventListener('click', async () => {
$('calculate-db-btn').disabled = true; $('calculate-db-btn').disabled = true;
status('Calculating voice loudness…'); const _calcTarget = (_bulkSelected && _bulkSelected.size > 0) ? `${_bulkSelected.size} selected` : 'visible';
status(`Calculating voice loudness (${_calcTarget})…`);
try { try {
const d = await clientCalculateVoiceDb(); const d = await clientCalculateVoiceDb();
renderVoiceList(); renderVoiceList();
@ -907,9 +910,11 @@ $('copy-active-voices-btn').addEventListener('click', async () => {
// (re)builds it from the wav when missing — so this is purely a warm-up. // (re)builds it from the wav when missing — so this is purely a warm-up.
$('precompute-embeddings-btn')?.addEventListener('click', async () => { $('precompute-embeddings-btn')?.addEventListener('click', async () => {
const backend = libraryTtsBackend(); const backend = libraryTtsBackend();
const ids = activeVoiceIds(); const _useSelected = _bulkSelected && _bulkSelected.size > 0;
if (!ids.length) { toast('No active voices to precompute', 'error'); return; } const ids = _useSelected ? [..._bulkSelected] : activeVoiceIds();
if (!confirm(`Precompute speaker embeddings for ${ids.length} active voice(s) via “${backend}”?\n\nThis warms each voice so the engine caches its .pt and first playback is instant.`)) return; const _scopeLabel = _useSelected ? `${ids.length} selected` : `${ids.length} active`;
if (!ids.length) { toast('No voices to precompute', 'error'); return; }
if (!confirm(`Precompute speaker embeddings for ${_scopeLabel} voice(s) via “${backend}”?\n\nThis warms each voice so the engine caches its .pt and first playback is instant.`)) return;
const btn = $('precompute-embeddings-btn'); if (btn) btn.disabled = true; const btn = $('precompute-embeddings-btn'); if (btn) btn.disabled = true;
const ov = document.createElement('div'); const ov = document.createElement('div');