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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-04 23:17:59 +02:00
parent 42a45c43b6
commit d1969b7609
4 changed files with 30 additions and 7 deletions

View File

@ -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 <n>" 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

View File

@ -1 +1 @@
1.12.93
1.12.94

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.93">
<meta name="app-version" content="1.12.94">
<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.93">
<link rel="stylesheet" href="/static/style.css?v=1.12.94">
<!-- ── 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.93"></script>
<script src="/static/loader.js?v=1.12.94"></script>
</body>
</html>

View File

@ -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 <n>" 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);