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
### Fixed

View File

@ -1 +1 @@
1.12.17
1.12.19

View File

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

View File

@ -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.17">
<link rel="stylesheet" href="/static/style.css?v=1.12.19">
<!-- ── 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.17"></script>
<script src="/static/loader.js?v=1.12.19"></script>
</body>
</html>

View File

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

View File

@ -613,10 +613,9 @@ async function loadVoiceLibrary() {
_libraryLoadPromise = (async () => {
setBusyButton('refresh-voices-btn', true);
const list = $('voice-list');
if (list) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8);
$('voice-count').textContent = 'Loading voices…';
updateLibraryInsights('loading');
status('Loading voice library…');
const silent = _voices.length > 0; // already rendered — refresh without blanking the list
if (list && !silent) list.innerHTML = loadingMarkup('Loading voice library', 'Scanning voices, reference text, metadata, ratings, and benchmark results.', 8);
if (!silent) { $('voice-count').textContent = 'Loading voices…'; updateLibraryInsights('loading'); status('Loading voice library…'); }
try {
const r = await fetch('/api/voices');
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();
status(`Loaded ${_voices.length} voices`);
} 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';
updateLibraryInsights('error');
status('Voice library load failed');
status('Voice library load failed: ' + e.message);
throw e;
} finally {
setBusyButton('refresh-voices-btn', false);
@ -926,7 +925,7 @@ function mergeBenchmarkResults(d) {
if (hit && hit.benchmark) v.benchmark = hit.benchmark;
});
}
window.loadVoiceLibrary = () => loadVoiceLibrary();
window.loadVoiceLibrary = loadVoiceLibrary;
window.mergeBenchmarkResults = mergeBenchmarkResults;
function activeBenchmarkVoices() {

View File

@ -22,23 +22,10 @@
<div id="lib-plays-list" class="reh-bookshelf"></div>
</div>
<!-- Characters / Cast -->
<!-- Characters / Cast — navigates to s-characters to avoid duplicate IDs -->
<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">
<label class="cl-tool-field">
<span class="cl-tool-label"><span class="mdi mdi-tag-multiple-outline"></span> Production</span>
<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 style="padding:32px;text-align:center;color:var(--subtext);font-size:14px">
<span class="mdi mdi-account-box-multiple-outline" style="font-size:40px;display:block;margin-bottom:12px;opacity:0.4"></span>
<p>Opening Characters&hellip;</p>
</div>
</div>