Studio -> Magazines: library, contents tree grouped by section with
confidence/pages/listening-time/audio status, the Stage D review
screen (tick/untick layout blocks, correct title/author, approve or
reject), a reflowed article view with click-to-seek sentence
highlighting, a linked original-page view showing the source bounding
box, and a podcast panel (2-3 host pickers, script generation,
turn-by-turn playback).
Wired into the existing lazy-module system (module-manifest.json,
loader.js, nav.js) rather than a separate app. Reuses /api/tts-preview
for both narration and podcast turns -- no new audio pipeline.
Fixes found building this:
- The pane toggle used role="tab" without the full ARIA tabs pattern
(aria-selected, roving tabindex, arrow-key nav) -- fails WCAG 2.2 AA
and hid the buttons from role-based tooling. Replaced with a button
group + aria-pressed, matching the plain-click interaction it
actually has.
- Print layout cues ("(continued on page 8)") were being sent to TTS
verbatim -- narration would have spoken the parenthetical aloud.
Stripped from the narration copy only; layout.json keeps the
original for the review screen.
- has_podcast now comes from the articles listing so opening an
article without a generated script skips a doomed GET instead of a
routine 404 on every open.
Verified against the real API contract with Playwright (library,
review pre-selection, approval, sentence highlighting + PDF-page
linking, podcast approval gate, zero console errors across the flow)
and manually end-to-end against a real ingested fixture, screenshots
included. Full suite: 94 Python tests, 33/34 desktop Playwright tests
(one unrelated pre-existing flaky DOM-count assertion).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>