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:
parent
5fd1660c09
commit
8922496ec9
@ -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
|
## [1.12.98] — 2026-07-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#2563EB">
|
<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="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||||
@ -27,7 +27,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.98">
|
<link rel="stylesheet" href="/static/style.css?v=1.12.99">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
|||||||
</script>
|
</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.98"></script>
|
<script src="/static/loader.js?v=1.12.99"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1880,9 +1880,24 @@ $('reh-autodesign-btn')?.addEventListener('click', async () => {
|
|||||||
|
|
||||||
// 2) For each character, design + save a voice
|
// 2) For each character, design + save a voice
|
||||||
let done = 0;
|
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) {
|
for (const sp of designOnly) {
|
||||||
if (rehDesignCancelled) { toast('Cancelled', 'error'); break; }
|
if (rehDesignCancelled) { toast('Cancelled', 'error'); break; }
|
||||||
|
if (!rehState.cast[sp]) { done++; continue; }
|
||||||
const info = byName[sp.toUpperCase().trim()] || {};
|
const info = byName[sp.toUpperCase().trim()] || {};
|
||||||
const gender = (info.gender || 'N').toUpperCase().charAt(0).replace(/[^MFN]/, 'N') || 'N';
|
const gender = (info.gender || 'N').toUpperCase().charAt(0).replace(/[^MFN]/, 'N') || 'N';
|
||||||
const desc = info.description ||
|
const desc = info.description ||
|
||||||
@ -1948,11 +1963,15 @@ $('reh-autodesign-btn')?.addEventListener('click', async () => {
|
|||||||
// Refresh the broader voice library so avatars/pictures resolve
|
// Refresh the broader voice library so avatars/pictures resolve
|
||||||
if (typeof loadVoiceLibrary === 'function') await loadVoiceLibrary().catch(()=>{});
|
if (typeof loadVoiceLibrary === 'function') await loadVoiceLibrary().catch(()=>{});
|
||||||
|
|
||||||
|
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();
|
renderCastList();
|
||||||
populateNarratorSelect();
|
populateNarratorSelect();
|
||||||
if (prog) prog.hidden = true;
|
if (prog) prog.hidden = true;
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
if (!rehDesignCancelled) toast(`Designed ${done} voice${done!==1?'s':''} — tagged "${tag}" + Rehearser`, 'success');
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Match from library — LLM picks the best EXISTING voice for each character ──
|
// ── Match from library — LLM picks the best EXISTING voice for each character ──
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user