Fix silent Design-all crash in Script Rehearser (v1.12.99)

Server logs showed /api/analyze-characters succeeding on every attempt
but /api/voice-design never once being called - the bulk voice-design
loop accessed rehState.cast[sp].voice unguarded right after that
(multi-second, for a large cast) request resolved. Any cast-entry
change during that window threw an uncaught TypeError, silently
killing the whole run with no toast, no further requests, and the
button stuck disabled. Guarded the access and wrapped the remaining
flow in try/catch/finally so failures are visible and the UI always
resets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-05 23:15:56 +02:00
parent 5fd1660c09
commit 8922496ec9
4 changed files with 35 additions and 9 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.12.99] — 2026-07-05
### Fixed
- **"Design all" (Script Rehearser bulk voice design) silently did nothing** — confirmed via server logs: `/api/analyze-characters` succeeded every time, but `/api/voice-design` was never once called. Root cause: `rehState.cast[sp].voice` was accessed unguarded right after the (multi-second, for a large cast) analyze-characters request resolved; any cast-entry change during that window threw a TypeError with nothing to catch it, silently killing the whole run with no toast and no further requests. Guarded the access and wrapped the rest of the flow in a try/catch/finally so any future failure surfaces as a toast and always re-enables the button instead of leaving it stuck with no feedback.
---
## [1.12.98] — 2026-07-05
### Added

View File

@ -1 +1 @@
1.12.98
1.12.99

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.12.98">
<meta name="app-version" content="1.12.99">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,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.98">
<link rel="stylesheet" href="/static/style.css?v=1.12.99">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.12.98"></script>
<script src="/static/loader.js?v=1.12.99"></script>
</body>
</html>

View File

@ -1880,9 +1880,24 @@ $('reh-autodesign-btn')?.addEventListener('click', async () => {
// 2) For each character, design + save a voice
let done = 0;
const designOnly = speakers.filter(sp => rehState.cast[sp].voice !== 'me');
// rehState.cast[sp] used to be assumed always present since `speakers` came
// from Object.keys(rehState.cast) moments earlier — but the analyze-characters
// await above can take many seconds for a large cast, and if anything
// removes/replaces a cast entry while that request is in flight, the plain
// `rehState.cast[sp].voice` access below threw a TypeError with nothing
// catching it (this whole loop sits outside the try/catch above), silently
// killing the click with no toast and no further requests — exactly the
// "clicked Design all and nothing happens" symptom, confirmed by server
// logs showing analyze-characters succeeding but voice-design never once
// being called. Guard it and keep going instead of crashing the whole run.
// The outer try/finally below is the same idea one level up: ANY unexpected
// exception here used to vanish into the console with the button stuck
// disabled — now it surfaces as a toast and always resets the UI.
try {
const designOnly = speakers.filter(sp => rehState.cast[sp]?.voice !== 'me');
for (const sp of designOnly) {
if (rehDesignCancelled) { toast('Cancelled', 'error'); break; }
if (!rehState.cast[sp]) { done++; continue; }
const info = byName[sp.toUpperCase().trim()] || {};
const gender = (info.gender || 'N').toUpperCase().charAt(0).replace(/[^MFN]/, 'N') || 'N';
const desc = info.description ||
@ -1948,11 +1963,15 @@ $('reh-autodesign-btn')?.addEventListener('click', async () => {
// Refresh the broader voice library so avatars/pictures resolve
if (typeof loadVoiceLibrary === 'function') await loadVoiceLibrary().catch(()=>{});
renderCastList();
populateNarratorSelect();
if (prog) prog.hidden = true;
btn.disabled = false;
if (!rehDesignCancelled) toast(`Designed ${done} voice${done!==1?'s':''} — tagged "${tag}" + Rehearser`, 'success');
} catch (e) {
toast('Design all failed: ' + (e?.message || e), 'error');
} finally {
renderCastList();
populateNarratorSelect();
if (prog) prog.hidden = true;
btn.disabled = false;
}
});
// ── Match from library — LLM picks the best EXISTING voice for each character ──