Single-voice health endpoint (GET /api/voices/{id}/health), problems stored in
meta on import, shown in the Try-out voice panel; architecture note and docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Deterministic hash-based character portraits and reference sheets
- Implemented strict early Renaissance aesthetic controls and role-specific headwear rules
- Added audiobook batch export scripts and progress tooling
- Verified book profiles with a new 'confirmed' state tracking mechanism
- Updated Pillow dependency for robust image handling
- Configured .gitignore for audiobook and audio cache exports
The caster emits fragments of noun phrases as characters — "in seinem Inneren"
-> Inneren, "die Hand" -> Hand, "mein Geliebter" -> Geliebten, "der Wind" ->
Wind. Each became a cast entry with its own voice while the real speaker's line
was lost to a phantom; measured against a hand-corrected book this was a large
part of why a fresh cast produced 73 speakers where the human had 62.
Such lines are now reset to Unknown — visibly unresolved and still recoverable
by the repair passes, rather than silently wrong. Applied to a real cast:
5 phantoms removed, 73 -> 68 speakers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Consolidation ran only at cast completion, but recast/repair passes assign
speakers of their own and can re-introduce the variants just removed —
confirmed live: a freshly cast book gained "Alrik" alongside "Alrik von
Blautann" during auto-repair, splitting one character across two voices again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One character is referred to by several labels across a book — bare vs full
name, a title on its own, a stray fragment — and each variant became its own
cast entry with its own voice, so the character audibly changed voice
mid-scene. Measured against a hand-corrected book this accounted for more of
the disagreements than genuine misattributions.
Variants are folded together only when one label's words are a strict subset
of another's after stripping articles and titles, so "Weber" and "Alter Weber"
stay separate and unrelated names never merge. Applied to a real cast:
Sharraz -> Sharraz Garthai, Baronin -> Baronin Ira von Seewiesen,
Von -> Oberst Alrik von Blautann (40 lines, 58 -> 54 speakers).
Also moves the quotation-mark toggle into the A- / A+ toolbar row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CSS cannot target a character inside a text node, so the marks are now wrapped
in their own .ab-q element at render time and hidden with a single class on
body. Toggling is instant regardless of segment count, and the stored text is
never modified.
Also fixes the toggle looking inert on a freshly cast book: it previously only
added missing marks and never hid the ones the book already carried. Button
moved bottom-right as a larger labelled pill, clear of the search controls.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
German prose never quotes narration, so displaying the guillemets makes a
mislabeled line obvious at a glance. Extraction strips them from ~80% of
segments, so they are rendered rather than stored: dialogue rows display
wrapped in » «, lines that kept their own marks are left as-is, and the saved
text is untouched so synthesis and exports are unaffected. A toolbar button
toggles visibility and the choice persists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adjacent segments merge when they share a speaker, but an unattributed line
carries the speaker "Unknown", so two consecutive Unknown lines compared as
equal and were fused into a single block joined by a blank line. Confirmed
against the source text: Lysandra's line and Perdia's interruption — two
separate paragraphs in the book — became one segment, after which no
attribution pass could separate them and one voice would read both sides of
the exchange.
"Unknown" means the speaker is not known, never that two lines share one.
Narration and lines with a real speaker name merge exactly as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A trailing inquit names only the last speaker, so applying it to a block
holding multiple turns silently mis-credited the earlier one — a wrong
attribution is invisible where an Unknown is not. Such blocks are now skipped
by the grammar rules and left for the LLM pass or a manual split.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Darrags Stimme klang tonlos." after a quote names who just spoke, but the
voice-announcement rule only looked at the narration before a line, so this
mirror image stayed Unknown. Verified on the book (-> Darrag).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Driven by unresolved lines in a real German novel:
- AB_SPEECH_VERBS was missing many common inquit verbs (grunzte, versetzte,
unterbrach, drängte, herrschte, keuchte, erklärte, widersprach, warf ein …)
and had no thought verbs at all, so inner monologue ("dachte Lysandra") was
never attributed.
- A trailing inquit naming two people ("unterbrach die blonde Perdia Lysandra")
resolved to the wrong one; the grammatical subject speaks, so articles and
adjectives are skipped and the first name after the verb wins.
- A quote whose own inquit had been mistyped as dialogue could never resolve,
because the rule required the following segment to be narration. A segment
opening with a lower-case speech verb is an inquit whatever its type; it is
now accepted and its type corrected.
Verified on the book: resolves further lines with no LLM call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a book always restored the localStorage draft and then pushed it to the
server as a backfill, so a tab holding an older copy silently overwrote newer
work saved from anywhere else — confirmed live: a cast improved from 79 Unknown
lines to 25 was destroyed by opening the book in a stale tab. Local and server
drafts are now compared by savedAt, the newest wins and is cached locally, and
the server is only backfilled when the local copy is actually newer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps found by validating the repair chain against a real German novel:
Split quotes whose inquit uses a pronoun rather than a name (»Ja, ungewöhnlich«
+ ", antwortete er knapp.") stayed Unknown, because every existing rule
requires a literal name after the speech verb. The antecedent is unambiguous,
so it now resolves the same way the colon rule does — to the last character
named in the narration immediately before the quote. Verified on the book: 2
further lines resolved with no LLM call, including the reported case.
The repairNarration pass also only received the verification prompt when
launched from the menu button; called from auto-repair or programmatically it
fell back to the general casting prompt, which is not written for the "is this
narration actually speech?" question. It now defaults to the verification
prompt in every entry path.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Identify/Cast flyouts were hard-anchored to open above their trigger,
which suited the casting panel's bottom footer but clipped them off the top of
the screen in Studio, where the same toolbar sits near the top — the first two
entries were unreachable. They now pick the side with space, prefer opening
downwards, stay clamped inside the viewport and scroll if taller than it.
Also reorders the Identify menu to the sequence the passes should run in, with
the one-click Auto-repair cast first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>