# Changelog
All notable changes to **TTS Voice Creator — Clone and Design** are documented here.
Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned with [Semantic Versioning](https://semver.org/).
---
## [Unreleased]
---
## [1.12.99] — 2026-07-05
### Fixed
- **"Design all" (Script Rehearser bulk voice design) silently did nothing** — confirmed via server logs: `/api/analyze-characters` succeeded every time, but `/api/voice-design` was never once called. Root cause: `rehState.cast[sp].voice` was accessed unguarded right after the (multi-second, for a large cast) analyze-characters request resolved; any cast-entry change during that window threw a TypeError with nothing to catch it, silently killing the whole run with no toast and no further requests. Guarded the access and wrapped the rest of the flow in a try/catch/finally so any future failure surfaces as a toast and always re-enables the button instead of leaving it stuck with no feedback.
---
## [1.12.98] — 2026-07-05
### Added
- **Character table view** in Library → Characters/Cast (toggle next to "Import from SillyTavern") — one row per character with avatar, name, sex, line count, assigned voice's language, moral alignment bar, voice, tags, and check/cross indicators for whether the SillyTavern, TTS voice-design, and image prompts have been generated. Persists your choice between Cards/Table.
- **Bulk voice auto-assign** — a checkbox on every character card/row plus an "Auto-assign selected" button in each production's header, so you can voice-cast a batch of characters in one click instead of one at a time. Runs sequentially (not in parallel) so each assignment sees what the previous one just picked and doesn't hand out the same voice twice.
- **Character tags are now visible** on cards — every character is auto-tagged with the book it was extracted from (already happened silently); the cards just weren't showing it. Useful for spotting the same character recurring across different books.
- **Jump-to-source links on detail fields** — Backstory, Motivation, Relationships, etc. now show small numbered links next to the label when the sheet has a citation for that field; clicking one scrolls straight to the exact page + quote in "Quellen im Text" instead of making you search the full source list.
- **Gender is now an extracted character-sheet field** (male/female/nonbinary) — it existed in the UI already but the LLM was never actually asked for it, so every character silently showed the same default icon.
- **Line-count tracking (best-effort)** — casting from Read Aloud or Script Rehearsal now tallies each character's dialogue-line count into their sheet when it's available in memory, feeding the new table view's "Zeilen" column.
### Fixed
- **SillyTavern & Concept Art prompts still silently failed to generate** even after the earlier max_tokens increase — they're the last two fields in a four-field JSON object, so any truncation always cost the same two fields regardless of the ceiling. Split into two independent LLM calls (Voice Design + Image / SillyTavern + Concept Art), run concurrently, each with its own full token budget — a squeeze in one pair can no longer cost the other pair anything.
---
## [1.12.97] — 2026-07-05
### Fixed
- **German umlauts mojibake'd during live casting** ("Häfen" → "Häfen") — the streaming attribution endpoint decoded the LLM's SSE response with `requests`' guessed encoding, which falls back to Latin-1 when the upstream doesn't declare a charset. Forced UTF-8 explicitly.
- **"LLM Thinking" pane duplicated the passage text** for models that ignore the reasoning-preamble instruction and stream straight into JSON. It now only shows genuine `...` content when present, and otherwise labels the pane honestly ("Live Output (raw — no reasoning exposed)") instead of passing off raw JSON as thinking.
- **A−/A+ font-size buttons had no visible effect** — `.ab-cv-row` had a hardcoded `font-size: 13.5px` that always overrode the CSS variable the buttons set on an ancestor. Now scales with it.
- **Typing a new name + Enter in the "Assign to" popup silently did nothing** (after the first cast/recast run in a session) — the popup is a page-lifetime singleton, but its input's Enter/typing handlers closed over whichever run's `assignName` existed the *first* time the popup was created. Every later run's popup opens now repoint those handlers at the current run.
- **Click-and-drag assign stopped working after the first run**, for the same reason as above (drag pre-fills the popup, then confirming it hit the same stale closure).
- **"Split text to Unknown Speaker" split at the wrong spot** when the selected phrase (or a whitespace-trimmed variant) occurred earlier in the same paragraph — it searched for the text with `indexOf` instead of using the actual selection position. Now computes the exact DOM-range character offset, so it always splits where you dragged, regardless of repeated text elsewhere in the passage.
---
## [1.12.96] — 2026-07-05
### Fixed
- **Server crashes with "RuntimeError: No response returned"** — the static-asset caching middleware used `@app.middleware("http")` (Starlette's `BaseHTTPMiddleware`), which has a known bug: when a client disconnects mid-`StreamingResponse` (e.g. the new live-attribution SSE stream hitting its idle timeout), its internal task group races the disconnect and raises this error. Rewritten as plain ASGI middleware that only touches response headers via the raw `send` callable — it never wraps the response the way `call_next()` does, so the race is gone entirely.
- **Casting timeouts/405s during long runs** — the streaming attribution endpoint used its own lock, separate from the blocking endpoint's. That meant a stream request and its own blocking fallback could both fire into the LLM's single processing slot at once — exactly the "ghost request" pile-up the original lock existed to prevent. Both endpoints now share one lock. Stream failures are also logged server-side now (silent before).
### Changed
- **The "LLM Thinking…" pane now shows real reasoning**, not the JSON answer echoed back. The attribution prompt asks for a brief `...` rationale before the JSON when streaming; the client shows only that block and stops once it closes, instead of dumping the raw in-progress JSON (which mostly just reproduces the passage text).
---
## [1.12.95] — 2026-07-05
### Added
- **Watch the LLM think, live** — the "LLM Reading…" card now splits into two panes the moment the model starts responding: its raw output stream (reasoning + the answer JSON as it's written) on the left, the passage it's reading on the right. Backed by a new streaming endpoint (`/api/attribute-dialogue/stream`, SSE) that forwards both `reasoning_content` (thinking models) and `content` deltas; the prompt-building and answer-parsing are shared with the blocking endpoint so the two can never drift. Any stream failure falls back to the blocking endpoint automatically — with an inactivity timeout (reset on every received chunk) instead of an overall one, since a slow model legitimately takes minutes per passage but long silence means the stream died. Works in the main cast, Recast unknown, and 2nd Quality Run.
### Fixed
- **Resolver invented characters from scenery words** — the deterministic colon-rule's fallback accepted any "der/die + capitalized noun", turning "auf dem Platz", "die Gesichter", "eine Kleinigkeit" into speakers PLATZ/GESICHTER/KLEINIGKEIT. The fallback is now a closed whitelist of person/role nouns, plus a clause-subject pattern ("Marcian unterdrückte seinen Ärger und sagte:" → Marcian).
- **More Unknowns resolved, safely** — two new deterministic rules: the impersonal post-quote formula ("ertönte es plötzlich über ihm. Karyla hatte…" → Karyla), and strict two-person alternation (both nearest preceding dialogue lines named and different → the one who didn't just speak), guarded to never fire across page boundaries, beyond a short window, or when the preceding narration ends with ":" (that colon introduces someone the other rules couldn't name — alternation would be a guess, not a deduction). All screenshot failure cases verified against the exact book sentences.
---
## [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
- **Deterministic Unknown-resolution pass during casting** — measured on a full book, the LLM left ~44% of dialogue "Unknown" even with all deduction rules in its prompt, so the two most mechanical rules are now applied in code after each passage, where they can't be ignored: the colon rule (narration ending in ":" names the next quote's speaker — with a non-agent-noun stoplist so "rief in die Runde:" resolves to the Ork, not "Runde"), post-quote inquits ("fragte Uriens leise." after a quote), and the "Der Krieger, der gesprochen hatte" pattern. Only fills segments the LLM left Unknown; never overrides an actual attribution. Verified against the exact failure cases from the reported screenshots.
---
## [1.12.92] — 2026-07-04
### Fixed
- **Deduction rules never reached the LLM when a custom prompt was saved** — the saved casting prompt turned out to be the 2nd-Quality-Run *verification* prompt (pasted into the Prompt panel at some point), which tells the model it's reviewing an existing segmentation instead of doing first-pass attribution, and contains none of the speaker-deduction rules; the client-side prompt auto-upgrade found no anchor text in it and silently skipped. Two fixes: the server now appends the deduction rules to any prompt that lacks them (attribution quality no longer depends on prompt history), and the misapplied saved prompt was reset to the shipped default (backed up to `config/audiobook_prompt.backup.txt`).
- **Every "die"/"der" underlined as a character name** — a descriptive alias like "Die, die den Vampir verließ" gets comma-split into tokens, and the bare article "Die" became an alias that matched every article in the book. Bare articles/pronouns are now filtered out of alias token lists.
- **Raw `style="…"` HTML leaking into casting text** — name-highlighting replaced name-by-name over HTML that already contained the injected highlight spans, so a shorter alias could match *inside* a longer name's `data-name` attribute and corrupt the markup. All names are now matched in a single combined pass that never re-scans its own output.
### Added
- **Chapter headings render as headings in the casting feed** — short standalone narration that reads like a title ("Prolog", "1. Kapitel", an OCR'd headline band) is now bold, larger, and centered on the paper page.
- **Font-size controls in the casting toolbar** — A−/A+ buttons scale the paper pages' text; the size is shared with the Script Rehearser Stage (same persisted scale), so one reading size applies across both screens.
---
## [1.12.91] — 2026-07-04
### Fixed
- **No way back from Cast Characters to the casting script** — the Characters/Cast view's production bar now has a "Casting" button that returns to the active casting session (via the reader's cast-restore), the pipeline stepper now also renders on the Library section so prev/next navigation works from there too, and the stepper's "Cast Characters" stop now *navigates* to the Characters/Cast view instead of kicking off sheet generation as a side effect of clicking a navigation element.
- **Two of the four generation prompts silently came back empty** — the LLM's answer was truncated by a too-small output budget (1600 tokens for four prompts, one of them multi-section), the JSON parse failed, and the UI showed success with two empty boxes. The budget is now 4096 tokens, a cut-off answer is salvaged for the fields that did complete, an all-empty response is a loud server error instead of a silent success, and a partial result names exactly which prompts are missing so you know to hit Generate again.
### Changed
- **"Reading PDF… page N / M" progress pill enlarged and vertically centered** in the document pane (was a small pill pinned to the top edge), so long extractions have a clearly visible "still working" signal.
---
## [1.12.90] — 2026-07-04
### Fixed
- **Page freezes ("Page Unresponsive") after opening a book / the casting feed** — the recent "click any word to assign" feature wrapped every single word of every casting segment in its own `` with hover styles; at book scale (~1,500 segments) that meant ~100,000 extra DOM nodes rebuilt synchronously on every feed redraw, freezing the tab. The spans are gone: the word under the cursor is now found via the browser's native caret-position API (`caretRangeFromPoint`) and highlighted with a single reused overlay element — same click/hover/drag-to-assign behaviour, zero extra DOM.
- **Burst of identical export downloads** — clicks queued up while the page was frozen could all fire at once on the export button when the tab unblocked, spawning one download + save-dialog per queued click. The export now ignores re-triggers for 2 seconds (and the freeze itself is fixed above).
### Changed
- **"Export cast .md" is now "Export cast .zip"** — one zip bundle containing the cast as a readable Markdown script plus a Markdown sheet per character of the book (identity, appearance, personality, story, abilities, and the generation prompts), instead of a single cast file and no character sheets at all.
### Added
- **Generation Prompts on every character sheet** — a new section in the Character Library detail view with four fold-out, copy-ready prompt boxes: Voice Design (Qwen3 TTS), Character Image (profile portrait), SillyTavern character card, and Concept Art (turnaround/model sheet). A "Generate" button fills all four in one LLM call over the character's complete profile — the previous behaviour generated voice/image prompts passage-by-passage during sheet extraction, where the model only ever saw a fraction of the character. The boxes are editable in place (autosaved like every other sheet field) and the results are included in the cast .zip export.
---
## [1.12.89] — 2026-07-04
### Added
- **Quick "add alias" shortcut in the Casting sidebar** — hovering a character in the "Characters found" list now reveals a small tag icon; clicking it opens a tiny popup to add an "also known as" name (e.g. "Garthai" for "Sharraz Garthai") without leaving the casting screen. Writes through the same `clUpsert` the rest of the app uses, so the alias is immediately shared with Rehearser/Character sheets and starts getting recognized/underlined in the casting text right away.
---
## [1.12.88] — 2026-07-04
### Added
- **Pipeline stepper prev/next navigation** — small chevron buttons flank the stepper to step to the nearest reachable stage in either direction, instead of only being able to jump directly to a specific stop.
- **Footer engine chips are now clickable fly-up menus** — click the LLM/STT/TTS chip in the footer status bar to quickly switch the active model (LLM: fetches the live model list for the current endpoint) or backend (STT/TTS: applies your pick to every matching picker across the app) without hunting through Settings or each screen's own dropdown.
- **Click or drag a name inside the casting text to assign it** — every word in the narration/dialogue text is now hoverable and clickable, not just the speaker label. Clicking a word opens the assign popup pre-filled with it; dragging across several words (for a multi-word name the roster doesn't know yet, e.g. "Sharraz Garthai") pre-fills the full phrase; double-clicking an already-known name/alias assigns it immediately with no popup. Built on the existing text-selection infrastructure (the "select text to split this segment" feature) rather than a separate mechanism, so the two don't fight over the same drag.
---
## [1.12.87] — 2026-07-04
### Added
- **Real paragraph/chapter-break detection for PDFs** — extraction now flags where a new paragraph starts (`readerMarkParagraphBreaks`) by comparing each line's vertical gap against the page's typical line spacing; a heading/image band followed by a large gap before body text is caught by the same check. The break is preserved as a real blank line all the way through sentence-building, unit-grouping, `audiobookScopeText`, and `splitTextIntoChunks` — previously every paragraph and chapter heading in a book was silently joined into one run-on blob before the casting LLM ever saw the text. The attribution prompts (default, saved-prompt auto-upgrade, "2nd Quality Run", and the server-side fallback) now explain how to read the blank lines, including treating a short standalone line before one as a chapter heading rather than dialogue.
### Fixed
- **Merging two casting segments dropped their page number** — `_abMergedSegment` built a fresh segment object and never carried over `.page` from either side, so a merged row would silently render as if it belonged to whatever page card came before it. It now anchors to the earlier segment's page.
- **Export cast .md was a JSON dump with a Markdown label** — the exported file's entire content was one big fenced `json` code block; it's now an actual readable script (plain paragraphs for narration, `**SPEAKER** (emotion): "line"` for dialogue, grouped under page headings), on both the server export route and the client-side fallback used when a book has no server id yet.
### Changed
- **"Edit in Rehearser" renamed to "Edit Characters"** — the button always lands on Rehearser's Cast/voice-assignment screen, not general script editing, so the label now says what it does.
- **Casting text no longer edits via double-click** — only the pencil icon opens a row for editing now, so selecting/dragging across a name to assign a character (a much more common action) can't accidentally drop you into edit mode instead.
---
## [1.12.86] — 2026-07-04
### Changed
- **Speaker attribution taught the deduction rules it kept missing** — the casting prompt's attribution section grew from 6 to 10 rules covering exactly the patterns behind most false "Unknown"/"Narrator" assignments: the colon rule (an inquit sentence ending in ":" names the next quote's speaker), post-quote attribution across the segment boundary (»Quote« — "ertönte es über ihm. Karyla hatte …" → Karyla spoke), mandatory pronoun resolution to the last-named person of matching gender, the addressee rule ("X wandte sich an Y" → X speaks next, Y answers), strict two-person ping-pong carried through tag-less exchanges (where 'Unknown' is almost never right), and role designations ('Ork', 'Nachbar', 'Der Fremde') as valid speakers instead of 'Unknown'. Saved custom prompts are upgraded in place via the existing prompt-migration mechanism, and the "2nd Quality Run" verification prompt gets the same deduction toolkit.
- **Book language now auto-detected for casting** — the attribution request's language hint previously came only from a manual dropdown that usually sat empty; it now falls back to detecting the language from the book text itself, so German books always get the German grammar/wording instruction without any manual step.
---
## [1.12.85] — 2026-07-03
### Performance
- **Heading OCR renders only the top band, not the whole page** — the OCR pass rasterized every full page at 2.5× scale (~6M pixels for A4) and then cropped ~10% of it; the canvas is now sized to the band itself so the remaining ~90% is never rendered or allocated. Also: the Tesseract worker (tens of MB of WASM/language data) is now terminated after extraction instead of living for the whole session, and the top-gap scan no longer allocates a throwaway array per page.
- **Casting feed name-underlining no longer rebuilds its name index per segment** — the list of character names/aliases and their compiled regexes was recomputed for every rendered segment (O(segments × records) over a full book); it's now cached and invalidated only when the roster or character records actually change. The dialogue-splitting fallback also compiled a constant regex once per sentence-ending character; hoisted to a module constant.
- **Old casting drafts migrate their page numbers once at load** — drafts saved before segments carried a `.page` field were re-deriving page boundaries via text search on every feed redraw; they're now stamped once when the draft is applied, and the renderer is single-path.
### Changed
- **`[hidden]` now always hides, globally** — one root rule (`[hidden]{display:none!important}`) replaces the per-component patches this bug class kept requiring (`.ab-char-bar`, `.wf-stepper`, `.reader-extract-banner`, `.ab-castpanel-inline`, and ~20 others individually). New components can no longer reintroduce the empty-box-while-hidden bug.
- **Removed the racy cast-panel view override** — navigating to Read Aloud restored an active casting session via a deferred `setTimeout` that overrode whatever view the caller had just chosen, which needed a global suppress flag (`_readerSuppressCastRestore`) set from two unrelated places to defeat. The restore is now synchronous and respects an explicit view request, so the flag is gone and callers simply win by calling `showReaderView()` after `navTo()`. This also fixes the sidebar "Reader" item landing in the casting view instead of the reader while a cast was active.
- **Cards can declare their collapse default in markup** — `data-collapse-default="closed"` on a card is now read by the generic collapse mechanism, replacing reader.js writing another module's localStorage key derived from the card's heading text (which would have silently broken on any heading rename).
- **Deduplicated helpers** — casting's segment-join, colour (hue/hex/normalize), and alias-splitting logic now delegate to the canonical implementations (`audiobookJoinSegmentText`, `clNormalizeColor`/`clHslToHex`/`clNameHue`, `clSplitIdentityTokens`) instead of maintaining byte-identical private copies; the status bar's LLM endpoint resolution is now a single shared `statusLlmTarget()` instead of two identical inline copies.
- **Removed dead state** — `readerState.pdfParsing`/`pdfParsedPages`/`pdfParseTotal` (never set, only reset — including a toast suffix that could never appear) and the write-only `readerState.textExtracted` flag.
### Fixed
- **Pipeline stepper never hid on a fresh session** — the "show only when a document is loaded" guard still checked for the old `'pdf'` step key after the PDF/Text merge into "Source", so the always-available Source stop kept the strip permanently visible.
---
## [1.12.84] — 2026-07-03
### Fixed
- **Clicking "PDF" or "Text" in the pipeline stepper did nothing** — `navTo('s-reader')` auto-restores an active casting panel a tick later via a deferred `setTimeout` (nav.js), which silently undid the stepper's own `showReaderView('main')` call right after it ran. Now sets the existing `_readerSuppressCastRestore` flag (already used by the Library's "open a fresh book" flow for the same reason) before navigating.
### Changed
- **Merged the "PDF" and "Text" stepper stops into one "Source"** — both stops led to the exact same Reader screen, so distinguishing them added a step without adding a real destination. Named "Source" to match the label already used on the Reader's import card. The stepper is now 6 stops instead of 7.
---
## [1.12.83] — 2026-07-03
### Fixed
- **Casting feed merge/split/edit got slower as the merge work landed** — the new per-row inline-edit feature attached 2 extra event listeners to every segment row on every redraw, and merge/split rebuild the *entire* feed from scratch; for a 1000+ segment book that's thousands of extra listener attachments on every single merge/split. Moved editing to the same event-delegation pattern already used for merging and speaker assignment (one listener on the feed container, not one per row) — row creation is back to just building HTML.
- **Reader's playback transport bar stayed visible with nothing loaded** — `readerResetDoc()` hid the synth bar and the zoom/search toolbar but never hid the play/pause/skip transport bar, so a reset or fresh session showed "0 / 0" controls with no document — one of several boxes making the empty-state Reader screen feel cluttered.
### Changed
- **Pipeline stepper made prominent, like a guided tour** — bigger numbered circles, a current-step highlight ring, and completed steps now show a checkmark and stay tinted instead of all steps looking equally flat.
- **"Voice & synthesis settings" now collapsed by default** — it's secondary until you actually have a document loaded; this was the first thing filling the empty-state screen with a large expanded box before you'd done anything.
- **`.ab-castpanel-inline` background set to `var(--panel)`** explicitly, per spec.
---
## [1.12.82] — 2026-07-03
### Fixed
- **Duplicate "opening" notifications when reopening a saved book** — a toast ("Opening book…") and the "Reading PDF… page N / total" progress pill appeared at almost the same instant and visibly overlapped on screen. Removed the toast — the progress pill already says the same thing with more detail (live page count) and stays up for the whole load instead of auto-dismissing.
---
## [1.12.81] — 2026-07-03
### Fixed
- **Casting toolbar icons overlapping at narrower panel widths** — `.ab-cv-topbar` (character-selection bar + undo/redo + page navigation) never wrapped, so when all three were visible at once at a moderate panel width, its fixed-width buttons got crammed together instead of flowing to a second row. Added `flex-wrap: wrap`.
---
## [1.12.80] — 2026-07-03
### Added
- **First slice of the Read Aloud / Rehearser merge** — three pieces of the planned unified screen land in Casting audiobook now: (1) the "paper" page look — pages are now literal white/off-white sheets (Courier New, matching Script Rehearser's Stage exactly) inside the Casting border/shadow "stacked card" treatment, instead of a plain surface-coloured box; (2) the character sidebar is now collapsible to just avatar dots via a new chevron toggle, for more room when you don't need the full list; (3) every line in the casting feed can now be edited inline (pencil icon or double-click, Ctrl+Enter to save, Esc to cancel) — previously only speaker reassignment and segment merging were possible, not correcting the text itself. More pieces of the merge (shared page navigation, folding Script Rehearser's Stage into this same view) are in progress.
---
## [1.12.79] — 2026-07-03
### Fixed
- **Theater Play library cards silently failed to open at all** — the real cause, on top of the missing navigation fixed in 1.12.78: opening a saved rehearsal (from Library → Theater Plays, and from Rehearser's own Library tab's open/export buttons) called `rehDbOpen()`/`REH_STORE`, a raw-IndexedDB API left over from before the app migrated rehearsals to server-backed SQLite. Neither exists anymore, so every click threw a `ReferenceError` caught silently by the surrounding try/catch — `loadRecord()` was never even reached, which is also why the 1.12.78 navigation fix alone didn't fully solve it. All three spots now use `rehDbGetById()`, the current API.
## [1.12.78] — 2026-07-03
### Fixed
- **Theater Play library cards did nothing when clicked** — opening a saved rehearsal from Library → Theater Plays called `loadRecord()`, which loads the script/cast and switches to the Cast phase, but never navigated to the Rehearser section itself. Since you're sitting on the Library section when you click a card, the phase switch happened inside `#s-rehearser` while that whole section stayed hidden — so nothing visibly changed. `loadRecord()` now navigates to Script Rehearser first.
---
## [1.12.77] — 2026-07-03
### Fixed
- **Excessive "Unknown" speakers in Casting audiobook** — the attribution LLM's output budget had been tightened to roughly half its previous size (`len(text)*0.45+512` vs. the old `len(text)+1000`, both capped). Narration-heavy passages fit fine, but dialogue-dense scenes need far more output than input — every quoted line becomes a full JSON object — so the tighter cap truncated the response mid-JSON on exchange-heavy passages. The truncation-repair only salvages a cut that lands right after a complete segment, so most failures degraded the whole chunk to naive quote-splitting, which has no way to attribute a speaker and labels everything "Unknown". Restored the more generous budget.
---
## [1.12.76] — 2026-07-03
### Fixed
- **Bulk "Set tag" wiped a voice's existing tags** — applying a tag to multiple selected voices overwrote each voice's `tag` field outright instead of adding to it, so a voice tagged "Privat, Sillytavern" lost both tags the moment you bulk-tagged it with something new. It now merges the typed tag(s) into each voice's existing tag list instead of replacing it.
---
## [1.12.75] — 2026-07-03
### Fixed
- **Casting audiobook panel grew to fit its content instead of the browser window** — the inline casting view (`.ab-castpanel-inline`) had no bounded height, so `.ab-cv-body`'s `flex:1` had nothing to divide up; the feed grew with every passage cast, pushing the action bar ("Continue casting" / "Edit in Rehearser" / ...) further down the page and forcing a full-page scroll to reach it. It's now bounded to the viewport like the floating casting popup already was, so the feed scrolls internally and the header/action bar stay in view.
---
## [1.12.74] — 2026-07-03
### Fixed
- **Empty bar showing above Rehearser's Library tab (and potentially the Reader's "Extract Text" prompt)** — the new `.wf-stepper` and `.reader-extract-banner` rules set `display: flex` unconditionally, which beats the browser's default `[hidden] { display: none }` rule at equal specificity — the same bug class fixed previously for `.ab-char-bar`. Both now have an explicit `[hidden]` override so they actually disappear when hidden instead of rendering as an empty box.
---
## [1.12.73] — 2026-07-03
### Added
- **Explicit "Extract Text" step for PDF import** — importing a PDF now stops after loading and rasterizing its pages (fast — you can already scroll/zoom and look at it) instead of immediately and silently running text extraction. A new banner shows the page count and an "Extract Text" button; the OCR-headings checkbox now lives here since it only matters right before extraction runs. This makes the pipeline stepper's "PDF" and "Text" stops correspond to two real, distinct stages instead of one being a no-op. Reopening an already-read document from the library is unaffected — it still extracts automatically, since there's nothing to decide on a document you've already extracted before.
---
## [1.12.72] — 2026-07-03
### Added
- **Cross-workflow pipeline stepper** — a persistent strip above Reader/Casting and Rehearser now shows the full production pipeline (PDF → Text → Cast Audiobook → Cast Characters (optional) → Script Rehearser → Generate MP3s → Audiobook) as clickable stops. Every reachable stop jumps directly to that stage without losing state — previously "Open in Rehearser" was a one-way door with no way back except hunting through the sidebar, and re-entering the casting panel could lose track of where you were.
### Fixed
- **Casting feed silently merged pages after leaving and returning to the casting panel** — page cards ("PAGE 4", "PAGE 6"...) were reconstructed on every redraw by re-guessing each segment's position via a forward-only text search against the cached source text; if a segment's text didn't exactly re-match (LLM cleanup, dehyphenation, short lines), the search stalled and every later page boundary was silently dropped, folding several pages into one card. Segments are now stamped with their real page number at cast time and the feed redraws directly from that instead of re-guessing it.
---
## [1.12.71] — 2026-07-03
### Changed
- **"Not synthesised" highlight no longer paints the whole book red on import** — every sentence starts in the `pending` state, so a freshly-imported document showed wall-to-wall red before you'd asked for anything to be synthesised. The red tint (PDF overlay and text-mode word spans) now stays invisible until synthesis actually starts — clicking "Synthesise pages", pressing play, or reopening a saved document that already has some audio — at which point pending/synthesising/ready/reading colours all show normally to track real progress.
---
## [1.12.70] — 2026-07-03
### Added
- **Recover chapter headlines baked into a PDF as images** — pdf.js's text extraction only ever sees real text glyphs, so a stylised chapter-title graphic (e.g. "1. Kapitel", "Prolog") silently vanished from the reader with no trace it was ever there. The importer now flags any page whose real text starts well below the top edge, rasterizes just that band, and OCRs it with a locally-vendored Tesseract.js worker (German + English), splicing the recognized heading back into the sentence stream so it reads aloud and highlights normally. Toggle via the new "OCR headings" checkbox above the import dropzone (on by default).
---
## [1.12.61] — 2026-07-01
### Fixed
- **Cascading 429s during casting/recasting** — the frontend already sends attribution requests one at a time, but a client-side timeout only aborts the browser fetch; the backend's blocking call to the LLM kept running in its thread regardless, leaving a "ghost" request still occupying the LLM's processing slot. The next chunk (or a timeout's retry-in-halves) then fired into that busy slot and the LLM answered with 429, and the ghosts piled up faster than they drained. Outbound calls to the audiobook-attribution LLM are now serialized behind a backend lock so the app never has more than one real request in flight against the engine, no matter how many chunks or retries the frontend queues up.
- **Casting timeouts on slow/local models** — the attribution request timeout (90s) was tuned for fast hosted APIs, but a local model generating at ~15 tokens/sec can take several minutes to finish a single passage (up to ~4000 output tokens), so nearly every request was aborted client-side before the model replied. Attribution, retry, recast, and warmup timeouts are all raised (90s→6min, 60s→3min, 75s→4min, 3min→4min) to match realistic local-inference generation time.
---
## [1.12.60] — 2026-07-01
### Fixed
- **Casting crash strands the panel and breaks the reader** — an unexpected per-passage error (bad LLM response shape, mid-retry network failure) could throw uncaught out of the casting loop instead of degrading gracefully to quote-splitting for that passage. The escaped error left `_audiobook.running` false with the panel stuck mid-cast; clicking "Stop Casting" then navigated back to the reader, which hit an unrelated null-pointer bug while re-rendering the PDF and showed an empty import screen. Per-passage attribution is now wrapped so any unexpected error falls back to auto-detected dialogue and the cast continues or ends in the normal "stopped early" panel instead of crashing.
- **Character selection bar placement** — reverted to sit above the casting feed (previous change had moved it below).
---
## [1.12.59] — 2026-07-01
### Added
- **Resume interrupted audiobook casts** — a "Continue casting" button now appears after restoring an autosaved cast that was cut short (e.g. by a server crash), picking up at the passage it stopped on and keeping already-cast passages instead of requiring a full "Recast all".
### Changed
- **Casting panel layout** — the progress bar now sits at the bottom of the casting panel, just above the action buttons, with the "Live" jump-to-latest button above it instead of overlapping the top of the feed.
- **Casting feed reads as paginated pages** — each source page in the casting feed now renders as its own card (white surface, thin border, soft shadow, margin between cards) on a neutral canvas background, instead of a plain dashed "Page N" divider inline with the text.
- **Casting panel element placement** — the character selection bar now appears below the feed instead of above it, and the "N characters · M segments" summary now sits under the character sidebar instead of in the action-button footer.
### Fixed
- **Stale progress bar on restored casts** — reopening a finished or restored audiobook cast no longer re-shows the passage-count header and progress bar that the completed-state view had just hidden, which was also causing the "N% (Passage X of Y)" label to wrap and clip.
- **Interrupted casts silently reported as 100% done** — manually fixing a speaker name, or stopping a "Recast unknown"/"2nd Quality Run" pass, used to overwrite the saved draft's progress with a `done:-1,total:-1` sentinel that displayed as "100% complete", permanently hiding that the original cast never finished. Both now preserve the real progress instead, and a successful "Recast unknown" pass now persists its corrections (previously not saved at all).
- **Character selection bar showing as an empty box** — `.ab-char-bar` set `display:flex` unconditionally, which beat the browser's default `[hidden]` rule at equal CSS specificity, so the bar rendered as an empty box even while "hidden". Added an explicit `[hidden]` override.
- **Stale static assets after edits** — the app loads a prebuilt bundle (`static/dist/main.min.js`) in front of the individual `static/js/*.js` files, and the cache-busting version query string wasn't being bumped, so recent fixes weren't reaching the browser. Bundle rebuilt and version bumped to force a fresh fetch.
---
## [1.12.58] — 2026-06-30
### Fixed
- **Audiobook cast stalls** — per-passage LLM attribution now uses bounded UI timeouts and falls back to deterministic quote detection when a passage or retry half takes too long.
- **LLM server responsiveness** — audiobook attribution and character-sheet extraction now run blocking LLM HTTP calls in worker threads and honor a clamped `timeout_seconds` request value, so slow local LLM calls no longer block the whole app server event loop.
---
## [1.12.57] — 2026-06-30
### Fixed
- **Audiobook stop-casting flow** — stopping a book-to-audiobook cast now keeps the casting panel open, aborts the active request cleanly, and preserves completed passages instead of dropping back to the empty Reader import view.
- **Interrupted cast drafts** — partial audiobook casts now save their real completed chunk count so recovery no longer treats an interrupted cast as 100% complete.
---
## [1.12.56] — 2026-06-30
### Fixed
- **Engines local-card fetch race** — overlapping Docker container refreshes can no longer let an older failed request overwrite a newer successful render with `Could not reach server: Failed to fetch`.
- **Engines refresh resilience** — local engine cards retry once, keep the last good container list on transient network changes, and show an inline Retry button only when no cached data exists.
---
## [1.12.55] — 2026-06-30
### Added
- **SQLite-backed app state** — server settings, TTS routes, and voice-design presets now persist through `config/tts_creator.db` via a shared `app_state` table while continuing to mirror JSON files for compatibility.
- **SQLite voice library index** — `/api/voices` now reads the last indexed voice payload from SQLite and refreshes the filesystem scan in the background; first load and explicit refresh still rebuild from disk.
- **Voice index status endpoint** — `GET /api/voices/index` reports index count, age, timestamp, and refresh state.
### Performance
- **Faster voice library loads** — normal voice-list requests avoid rescanning every audio/meta/reference/image file on each page load; the Refresh button uses `/api/voices?refresh=1` when a full rescan is needed.
- **Shared voice dropdown cache** — TTS backend voice options now prefer the same SQLite index before falling back to filesystem scanning.
---
## [1.12.54] — 2026-06-30
### Fixed
- **Engine API keys now persist** — Docker/local engine card keys and cloud API cards save into server settings via `engine_api_keys`; Groq STT and ElevenLabs also sync to their existing first-class settings keys.
- **Engine settings whitelist** — `engine_local_urls`, `engine_container_names`, `engine_api_keys`, `custom_engine_cards`, and `fishspeech_url` are now accepted by the settings API instead of being silently dropped.
### Changed
- **Engines header cleanup** — replaced the wide right-aligned header note with a compact stacked context row so Language Models, Speech Recognition, and Text to Speech headers no longer leave a large empty band.
---
## [1.12.53] — 2026-06-30
### Changed
- **Library frontend split** — moved the Characters / Cast workspace out of `static/js/library.js` into `static/js/library-characters.js`; `library.js` now owns only production overview, book/play cards, and cross-links.
- **Removed obsolete monolith** — deleted the unused tracked `static/app.js`; the active runtime path remains `loader.js` + `static/js/*` or the generated bundle.
### Performance
- **Faster startup** — navigation now initializes before async settings/voice refreshes, benchmark data loads only when Performance opens, and ElevenLabs browsing starts only when Get Voices is shown.
- **Faster voice-list rendering** — batches voice-row DOM insertion with `DocumentFragment` and avoids rebuilding filter `