fix: restore My Voices after infinite recursion + settings crash (v1.12.18-19)

Root cause 1 (v1.12.18): `window.loadVoiceLibrary = () => loadVoiceLibrary()`
overwrites the global binding the arrow function references, causing immediate
RangeError: Maximum call stack size exceeded on every call. Changed to direct
assignment `window.loadVoiceLibrary = loadVoiceLibrary`.

Root cause 2 (v1.12.18): `loadSettings()` called `renderSettingsAbout()` which
lives in conversation.js (batch E), loaded after init.js. Guard added with
typeof check; nav.js already calls it safely when the About section opens.

Also (v1.12.18): s-library.html duplicated cl-book-filter / cl-search / cl-grid
from s-characters.html, breaking getElementById. Characters panel in Library
now redirects to s-characters instead of duplicating its DOM nodes.

Also (v1.12.19): engines.js triggers a second loadVoiceLibrary() after nav.js
already rendered voices, blanking the list briefly. Second call now silently
re-fetches without clearing the list when voices are already present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-06-28 01:57:49 +02:00
parent 24ee377230
commit ce0f4c43bc
7 changed files with 37 additions and 32 deletions

View File

@ -9,6 +9,23 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
--- ---
## [1.12.19] — 2026-06-28
### Fixed
- **Voice list flicker on load** — engines.js (batch E) calls `loadVoiceLibrary()` after nav.js already rendered voices, causing the list to blank out and reload. Second call now runs silently (no skeleton, no status reset) when voices are already present.
---
## [1.12.18] — 2026-06-28
### Fixed
- **Diagnostic code removed** — temporary debug IIFE and renderVoiceList try-catch scaffolding cleaned out of voice-library.js; My Voices loads correctly.
- **Settings load crash: renderSettingsAbout is not defined**`loadSettings()` called `renderSettingsAbout()` unconditionally but that function lives in `conversation.js` (batch E, loaded after init). Guard added with `typeof` check; the About section still renders when opened (nav.js already guards the same call).
- **Duplicate element IDs**`s-library.html` Characters panel duplicated `cl-book-filter`, `cl-search`, `cl-grid` from `s-characters.html`, causing `getElementById` to return the wrong element. Characters tab in Library now shows a redirect placeholder instead.
- **Infinite recursion in loadVoiceLibrary**`window.loadVoiceLibrary = () => loadVoiceLibrary()` overwrites the global binding that the arrow function references, causing immediate stack overflow. Changed to direct assignment `window.loadVoiceLibrary = loadVoiceLibrary`.
---
## [1.12.17] — 2026-06-28 ## [1.12.17] — 2026-06-28
### Fixed ### Fixed

View File

@ -1 +1 @@
1.12.17 1.12.19

View File

@ -65,11 +65,13 @@ def _is_sound_asset_file(path: Path) -> bool:
) )
_AUDIO_EXTS_SET = frozenset(_AUDIO_EXTS)
def _voice_audio_files(root: Path): def _voice_audio_files(root: Path):
for ext in _AUDIO_EXTS: # Single rglob pass instead of one per extension (6× faster directory scan)
for p in root.rglob(f"*{ext}"): for p in root.rglob("*"):
if not _is_internal_voice_file(p) and not _is_sound_asset_file(p): if p.suffix.lower() in _AUDIO_EXTS_SET and not _is_internal_voice_file(p) and not _is_sound_asset_file(p):
yield p yield p
def _find_voice_audio(voice_id: str, scan_dir: Path) -> Path | None: def _find_voice_audio(voice_id: str, scan_dir: Path) -> Path | None:

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.12.17"> <link rel="stylesheet" href="/static/style.css?v=1.12.19">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── --> <!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -336,7 +336,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.12.17"></script> <script src="/static/loader.js?v=1.12.19"></script>
</body> </body>
</html> </html>

View File

@ -490,7 +490,7 @@ async function loadSettings() {
} }
await refreshTtsBackendAvailability(); await refreshTtsBackendAvailability();
enhanceSettingsHelp(document); enhanceSettingsHelp(document);
renderSettingsAbout(); if (typeof renderSettingsAbout === 'function') renderSettingsAbout();
} }
function markSettingsSeen() { function markSettingsSeen() {

View File

@ -613,10 +613,9 @@ async function loadVoiceLibrary() {
_libraryLoadPromise = (async () => { _libraryLoadPromise = (async () => {
setBusyButton('refresh-voices-btn', true); setBusyButton('refresh-voices-btn', true);
const list = $('voice-list'); const list = $('voice-list');
if (list) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8); const silent = _voices.length > 0; // already rendered — refresh without blanking the list
$('voice-count').textContent = 'Loading voices…'; if (list && !silent) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8);
updateLibraryInsights('loading'); if (!silent) { $('voice-count').textContent = 'Loading voices…'; updateLibraryInsights('loading'); status('Loading voice library…'); }
status('Loading voice library…');
try { try {
const r = await fetch('/api/voices'); const r = await fetch('/api/voices');
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); } if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); }
@ -628,10 +627,10 @@ async function loadVoiceLibrary() {
if (typeof renderPerfHistory === 'function') renderPerfHistory(); 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;font-size:13px">Failed to load voices: ' + (e.message||String(e)) + '</div>';
$('voice-count').textContent = 'Load failed'; $('voice-count').textContent = 'Load failed';
updateLibraryInsights('error'); updateLibraryInsights('error');
status('Voice library load failed'); status('Voice library load failed: ' + e.message);
throw e; throw e;
} finally { } finally {
setBusyButton('refresh-voices-btn', false); setBusyButton('refresh-voices-btn', false);
@ -926,7 +925,7 @@ function mergeBenchmarkResults(d) {
if (hit && hit.benchmark) v.benchmark = hit.benchmark; if (hit && hit.benchmark) v.benchmark = hit.benchmark;
}); });
} }
window.loadVoiceLibrary = () => loadVoiceLibrary(); window.loadVoiceLibrary = loadVoiceLibrary;
window.mergeBenchmarkResults = mergeBenchmarkResults; window.mergeBenchmarkResults = mergeBenchmarkResults;
function activeBenchmarkVoices() { function activeBenchmarkVoices() {

View File

@ -22,23 +22,10 @@
<div id="lib-plays-list" class="reh-bookshelf"></div> <div id="lib-plays-list" class="reh-bookshelf"></div>
</div> </div>
<!-- Characters / Cast --> <!-- Characters / Cast — navigates to s-characters to avoid duplicate IDs -->
<div class="lib-panel" data-library-panel="characters"> <div class="lib-panel" data-library-panel="characters">
<div class="cl-toolbar card" style="padding:12px; display:flex; gap:10px; align-items:center; flex-wrap:wrap"> <div style="padding:32px;text-align:center;color:var(--subtext);font-size:14px">
<label class="cl-tool-field"> <span class="mdi mdi-account-box-multiple-outline" style="font-size:40px;display:block;margin-bottom:12px;opacity:0.4"></span>
<span class="cl-tool-label"><span class="mdi mdi-tag-multiple-outline"></span> Production</span> <p>Opening Characters&hellip;</p>
<select id="cl-book-filter"><option value="">All productions</option></select>
</label>
<label class="cl-tool-field" style="flex:1; min-width:180px">
<span class="cl-tool-label"><span class="mdi mdi-magnify"></span> Search</span>
<input id="cl-search" type="text" placeholder="Name, alias, archetype, tag…" spellcheck="false">
</label>
</div>
<div id="cl-grid" class="cl-grid">
<div class="cl-empty">
<span class="mdi mdi-account-box-multiple-outline" style="font-size:48px;opacity:0.4"></span>
<p>No characters yet.</p>
<p class="cl-empty-hint">Run <b>Character sheets</b> from Read Aloud or the Script Rehearser to populate your cast.</p>
</div>
</div> </div>
</div> </div>