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);