From d1969b76097a49fa469f7bd1ebf097ea6a47514f Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sat, 4 Jul 2026 23:17:59 +0200 Subject: [PATCH] Fix quality-run roster display, blank API errors, resolver-first (v1.12.94) The recast/quality view's sidebar rebuilt its roster from only the lines being checked, appearing to wipe every named character (the cast itself was safe: named lines are never recast targets and increases in Unknowns already roll back). The sidebar now seeds from the full cast and refreshes during the run. Error notes fall back to the HTTP status (statusText is empty on HTTP/2). The deterministic grammar resolver runs before any LLM call in quality runs. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 11 +++++++++++ VERSION | 2 +- static/index.html | 6 +++--- static/js/audiobook.js | 18 +++++++++++++++--- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 916203a..8da7e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,17 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.12.94] — 2026-07-04 + +### Fixed +- **2nd Quality Run / Recast unknown appeared to wipe all named characters** — the sidebar roster was rebuilt only from the lines being checked, so it collapsed to "Unknown " as if the whole cast was lost. The cast itself was never touched (named lines are never targets, and a run that would increase Unknowns already rolls back completely) — but the display now seeds from the full cast and stays correct as the run progresses. +- **"API Error while checking Unknown line:" with no reason** — `statusText` is empty on HTTP/2, so error notes ended blank; they now fall back to the HTTP status code. + +### Changed +- **Quality runs apply the grammar rules first** — the deterministic resolver (colon rule, post-quote inquit, "who had spoken") now runs before any LLM call in Recast unknown / 2nd Quality Run, resolving the mechanical cases instantly and shrinking the LLM's queue. + +--- + ## [1.12.93] — 2026-07-04 ### Added diff --git a/VERSION b/VERSION index 0fec8f2..7c2b16b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.93 +1.12.94 diff --git a/static/index.html b/static/index.html index 28dd8ca..b8b4cab 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 638fd07..af29361 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -2663,6 +2663,10 @@ STRIKTE FORMAT- UND TEXTREGELN: }); return { + // Refresh the sidebar roster from the FULL cast — recast/quality runs feed + // the view only the lines they check, which made the sidebar collapse to + // "Unknown " as if all named characters had been lost. + recountRoster(allSegs) { _abRecountRoster(allSegs || []); }, rebuild(segs) { this.clearProcessing(); feed.querySelector('.ab-skel-feed')?.remove(); @@ -3042,6 +3046,11 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {}) const countUnknownDialogue = arr => (arr || []).filter(s => s?.type === 'dialogue' && (!s.speaker || /^Unknown|Unbekannt/i.test(s.speaker))).length; const beforeUnknownCount = countUnknownDialogue(segs); + // Mechanical cases first (colon rule, post-quote inquit) — resolved in code + // for free, so they never even reach the LLM queue below. + const preResolved = audiobookResolveUnknowns(segs, [], _audiobook.roster || []); + if (preResolved.length) toast(`${preResolved.length} Unknown line${preResolved.length !== 1 ? 's' : ''} resolved by grammar rules`, 'success'); + const unknownIdxs = []; for (let i = 0; i < segs.length; i++) { if (segs[i].type === 'dialogue' && (!segs[i].speaker || /^Unknown|Unbekannt/i.test(segs[i].speaker))) { @@ -3065,7 +3074,8 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {}) const groups = audiobookRecastGroups(unknownIdxs, segs); const view = audiobookCastView(unknownIdxs.length, llm_url, model); - + view.recountRoster(segs); // sidebar shows the whole cast's roster, not just the lines being checked + view.processing('Waking up LLM model (this may take a few minutes if cold-booting)…'); try { await audiobookFetchWithTimeout('/api/attribute-dialogue', { @@ -3157,11 +3167,12 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {}) timeout_seconds: audiobookTimeoutSeconds(AUDIOBOOK_RECAST_TIMEOUT_MS) }, promptOverride)) }, AUDIOBOOK_RECAST_TIMEOUT_MS + 5000); - if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || e.error || r.statusText); } + if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || e.error || r.statusText || ('HTTP ' + r.status)); } data = await r.json(); } catch (err) { if (err.name === 'AbortError') { _audiobook.cancel = true; break; } - view.note(`API Error while checking ${group.length > 1 ? 'a group of Unknown lines' : 'Unknown line'}: ${err.message}`); + // statusText is empty on HTTP/2, which used to leave this note blank after the colon + view.note(`API Error while checking ${group.length > 1 ? 'a group of Unknown lines' : 'Unknown line'}: ${err.message || err}`); for (const idx of group) { view.addSegments([segs[idx]]); done++; @@ -3207,6 +3218,7 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {}) if (groupsSinceSave >= 10 && _audiobook.lastText) { groupsSinceSave = 0; _abSaveDraft(_audiobook.segments || [], _audiobook.roster || [], _audiobook.lastText, _audiobook.completedChunks || 0, _audiobook.completedTotal || 0); + view.recountRoster(segs); // keep the sidebar reflecting the full cast as lines resolve } } view.update(_audiobook.cancel ? done : unknownIdxs.length);