# 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] ### Changed - **Characters: global cast view in the sidebar, per-book characters inside Studio.** *Library > Characters / Cast* is now titled "All characters (all books)": one collapsible band per book (heading, per-book count, `aria-expanded`), first book open and the rest closed and not rendered until opened (DOM budget), and a *Search all books* field. Studio > Voices lists only the book picked in *Source*, under a "Book: " chip with **Show characters of all books**; the Characters step shows the same chip; with no book picked Voices shows a prompt (Pick a book / show all books). Per-book actions stay in the band headers. Tests: `tests/characters-scope.spec.js`. --- ## [1.58.0] — 2026-10-03 ### Added - **Magazine UI shows page labels, display titles and merge suggestions.** The article index (list, playlist, driving-mode Index sheet and driving header) shows `display_title` (falls back to `title`) with the `page_label` ("Seite 33") in smaller muted text, plus small kind badges (advert, photo, contents, imprint, continuation) and an "Audio is out of date: re-render" badge when `audio_stale` is set. Review shows a suggestion box: the proposed merge target with confidence and a collapsible plain-language "Why do we think so?" list, **Merge into previous article** (`POST .../merge-into-previous {into}`; a 409 "proposal changed" refreshes the suggestion; undoable) and **Use suggested title** (`POST .../accept-title`; undoable). Controls are labelled, keyboard operable and 44 px high. Tests: `tests/magazine.spec.js` ("untitled entries", driving index). - **Untitled magazine entries ("Page 33") are explained, merged on request and given readable titles** (`core/magazine_untitled.py`, `scripts/magazine_untitled.py`). Root cause: the review screen creates a `Page N` stand-in for any page no article covers, and *Approve* without a toggled box saved only the visible page's regions, which cut multi-page features down to one page. On *c't KI Wissen 2026* this left 59 stand-ins, most of them continuation pages. A continuation detector scores each untitled entry against the article before it (lower-case/mid-sentence start, hyphenated break, the narration fusion rule, open ending, adjacent page, column width; a signed-off previous article or the page's own headline counts against). An optional local-LLM check runs only for ambiguous scores. Each proposal comes with a confidence and the evidence. Records whose title repeats the headline of the article they continue are proposed as well. Titles: a deterministic fallback (*Anzeige*, *Bildseite*, *Inhalt*, *Impressum*, *Editorial*, *Textfragment*, the page headline, or the first sentence cut to 8 words), plus optional validated and cached LLM titles in the magazine's language (`magazine_llm_titles`, `--no-llm`). Nothing changes silently. `GET /articles` gains derived `display_title`, `title_source`, `suggested_title`, `page_label`, `untitled_kind`, `merge_suggestion` and `audio_stale`, and `GET /untitled` plus `quality/report → untitled_articles` list everything. Changes happen only through the undoable `POST /articles/{id}/merge-into-previous` and `POST /articles/{id}/accept-title` (never over a real title), `POST /titles/suggest` (suggestion cache only), and the CLI's `--apply` (backup first). A merge keeps reading order, marks the article `audio_stale` (a fresh render clears it) and deletes no audio. See architecture/magazine-ingestion.md, "Untitled entries". - **Distinct, deterministic face profile per character** (`core/face_profile.py`). The cast of one book looked like one family because most sheets name no facial feature and the image model then draws its default face. Each character now gets a compact `face:` clause (face shape, jaw, nose, eyes, brows, mouth, cheekbones, skin, hair colour and style, facial hair, age signs, a mark) in the Subject of every character prompt: traits the sheet states (German or English) are kept, the rest comes from species/age/gender/culture-filtered option pools, chosen so that the characters of a book are spread apart (greedy max-distance in creation order; adding characters never changes an existing profile; relatives named in `relationships` share a few traits). Owner override: a `visual_face` text on the sheet. On the real books 02/03 the mean pairwise face-descriptor distance goes from 0.09/0.04 to 0.91 (44/68 characters had no face trait at all). See `architecture/character-visual-identity.md`. - **Dry run for faces:** `POST /api/character-face-preview {book}` lists every character's face profile (what came from the sheet, what was chosen) with a similarity matrix and before/after score; `POST /api/character-prompt-preview` also returns `face_clause`. Nothing is generated or saved. - **Character info is translated to English before image prompts are built** (`core/prompt_translation.py`, `_english_prompt_inputs`). Free-text sheet fields (species, appearance, clothing, age, props, story-moment quote) and the book setting are translated in one batched, low-temperature call through the app's existing LLM, validated per field (non-empty, no error echo, length, names preserved, not still German), and cached in the new `prompt_translation_cache` table keyed by source hash, language, model and prompt version. Records and the displayed German text never change; species/age/dwarf/period checks now run on English. If the LLM is down or output is rejected the original text is used, a warning is logged, `prompt_translation.used_untranslated_text` is reported and generation continues. `PROMPT_TRANSLATION=off` disables it. New read-only dry run `POST /api/character-prompt-preview`. See architecture/character-visual-identity.md. - **Record/replay cache for vision OCR responses** (`core/vision_cache.py`, `MAGAZINE_VISION_CACHE=live|record|replay`). Freezes the raw model response per (model, prompt, image, parameters) in a git-ignored cache directory so OCR/layout/post-processing changes can be compared on identical vision output; replay fails loudly on a miss. `bench_ocr.py --vision-cache` and `compare_magazines.py` (`--baseline-vision-run`, `--new-vision-run`) report mode and models and say whether a comparison is frozen. See architecture/magazine-ingestion.md. - **Driving-mode menu redesigned.** Sections *View*, *Show*, *Playback* and *Theme*. *View* is a radio group with icon, name and indicator: **Picture** (whole picture, no text), **Text** (whole text, lyrics-style scrolling, no picture) and **Split** (picture plus three lines; the former Compact). The on/off options (*Show text*, *Show pictures*, *Minimize text and picture*) are real switches (label left, switch right, thumb position and a tick/cross inside the thumb as well as colour; `role="switch"`). The theme is one segmented control **Light | Auto | Dark** with icons and a tick on the selected segment. Controls are 44 px high, the flyout fits 320 px, text keeps 4.5:1 and switch, segment and radio boundaries and the focus ring 3:1 in both themes; the thumb does not slide with reduced motion. Keyboard: arrows (Up/Down/Left/Right) move over every control, Home/End jump, Space/Enter activate, Escape closes and returns focus to the menu button. Views stored by older builds migrate (image to picture, lyrics to text, compact to split). - **Driving mode plays on automatically.** When an article ends it is marked finished (saved like the playlist does) and the next article that has audio starts (rows without audio are skipped, speed carries over, title, picture, text, index highlight and lock-screen metadata follow, "Now playing: ..." is announced once). At the last article it stops with an *End of magazine* state, no wrap-around. New switch *Play next article automatically* (default on, remembered in the driving preferences); off stops after the article as before. A next article whose audio cannot be loaded is skipped with a brief message. Manual Next, seeks and duplicate `ended` events never advance twice. - **Driving mode: choose how much text is shown.** New menu section *Text in Split view*: **Sentence** (default; only the current sentence, large), **Context** (previous, current, next: the former three lines) or **Paragraph** (the whole paragraph, current sentence bold and underlined, spoken word marked). Stored in the driving preferences (invalid values fall back to Sentence); the Text view still shows the whole text. Tests: `tests/magazine.spec.js` ("Driving mode text fit and text amount"). ### Changed - Prompt wording that pulled every face and head toward one look was removed: "consistent facial construction" (concept style) became "individually distinct faces"; the hair locks no longer force "reaching at least to the ears or collar" / "past the shoulders" when the face clause states the hair; the dwarf body lock no longer says "a thick, full beard" (the beard now comes from the face clause, men only, so dwarf women get none); the Subject limit in the compact render prompts is 900 instead of 700 characters to make room for the clause. - `vision_ocr_page` routes its request through the cache layer; the default `live` mode behaves exactly as before. ### Fixed - **Approving a review page keeps all blocks of a multi-page article.** *Approve* without touching a box saved only the regions shown on the current page, so a multi-page article shrank to that page, and *Approve & Next* then created a `Page N` stand-in on the next page. `selectedBlockIds()` now starts from the article's saved blocks (plus visible ticks) unless ticks were changed; explicit tick selections behave as before. Test: `tests/magazine.spec.js` ("approving a multi-page article page by page"). - **Driving mode text ran out of its frame.** A very long sentence (300+ characters) in the Split view was cut off at the bottom. The text frame now takes the space between picture and controls (flex, `min-height: 0`), the current text fits automatically (1.5 rem down to 1 rem, in rem so text scaling and a 200% root font count), the picture shrinks first, and text that still does not fit scrolls inside the frame and follows the spoken word instead of being clipped. Verified at 320x568, 375x667, 414x896 and 667x375 (also with a 200% root font) in all three amounts. - **"Colors: Dark (night)" looked disabled in the light theme.** The menu item carried `data-theme="dark"`, which is also the hook of the app-wide dark palette (`[data-theme="dark"] {...}`), so the button itself got the dark palette's light text colour on the light menu. The theme buttons now use `data-theme-choice`, so no app theme selector can match them. - **A boy rendered as an adult:** `_character_age_category` read `age`, but sheets store `age_estimate`. - **Characters' data no longer bleeds into each other through shared labels or names.** A real-library audit (270 records) traced Arthag's (book 02) copy of Kolon's species and relationships and 16 whole-sheet copies (Zwerg/Kolon = Arthag, Mann = Fremder, Alchimist = Promos ...) to identity matching on any shared token ("Zwerg", "der Zwerg", "Mann") with "longer text wins" merging, and to `csRosterCanonicalName` renaming a sheet to the first cast name found among its aliases instead of its own name. Now only a real name links two profiles (`static/js/character-identity.js`, `core/character_records.py`), the own name always wins (`csFindMergeKey`, `csRosterCanonicalName`, `clUpsert` writes to the exact-name record before any alias scan, no seed rename onto a name that has its own record). Tests: `tests/character-identity.test.mjs`. - **Other cast members' names are no longer stored as a character's alias or real name** (Kolon carried "Sharraz Garthai"/"Sharraz"): `_charsheets_parse` drops them in cast-target mode, keeping relations such as "Tochter des Promos". - **Model "thinking" no longer lands in profile fields** ("Warte: ...", "Nein, Kolon ist ...", "Der Text sagt ..."): every text field is cut before such markers on extraction. - **The story-moment picture no longer shows another character's scene:** only a source quote that names the character is used, otherwise its own mannerisms (16 of 270 lower panels named only another cast member). - **Age is read from German and English wording:** "Junge", "Mädchen", "boy", "girl", number words and "Sommer/Winter" ages, the estimate's own "im Text als Junge beschrieben" clause, and age-tagged quotes; Marrad ("fast acht Sommer") is now a child. "15 Jahre älter" is no longer read as a 15-year-old. - **New read-only audit and dry-run cleanup for saved characters:** `scripts/audit_character_records.py`, `scripts/clean_character_records.py` (dry run by default; `--apply` writes a backup first). See architecture/character-extraction.md. Tests: `tests/test_character_record_hygiene.py`. - **German "hat" (has) became a hood:** "hat dünne Arme" turned into "soft wool hood" in the prompt; `_period_wording` now only maps English "hat" and leaves German text alone. - **Dwarf body shape and beard no longer leak onto human characters' images.** The dwarf body lock ("one adult dwarf", short legs, thick full beard) was switched on by the word "Zwerg" anywhere in the species field, including extraction notes about other characters. In the real library this hit Marrad (a thin human boy, "Mensch (oder Zwerg, je nach Kontext …)") and Kolon and Arthag in book 02 ("Mensch (… 'Der Zwerg' wird separat erwähnt … Warte: 'Der Zwerg lächelte grimmig' …)"), whose prompt Subject also carried that sentence about another character. Species now comes only from the field's own leading assertion: long or quoting bracket notes are dropped, alternatives ("Mensch oder Zwerg") never count as a dwarf, and a *visual species* override is honoured for the body lock too. Seven of 270 records change; tests in `tests/test_character_species_isolation.py`. --- ## [1.57.0] — 2026-10-03 ### Added - **Night theme for driving mode.** A *Colors* group at the end of the display menu: Auto (follows the system, live; default), Light, Dark; remembered in this browser (storage errors are tolerated) and independent of the app's own theme. Dark uses true black, text at about 85% white, a muted blue accent, dimmed pictures (brightness .85), a themed top bar, menu, index with rows, lyric lines and current-line highlight, spoken-word marker, slider, dock buttons (still perfect circles), zoom buttons, *Tap to play*, toasts and share box, and sets the browser theme colour while driving mode is open (restored on exit). All text pairs are at least 4.5:1 and borders 3:1, including *no audio* rows; status icons differ by shape, not just colour. The overlay is painted dark from its first frame. Tested with Playwright (persistence, Auto vs emulated colour scheme, computed-style contrast ratios, first-frame background, theme-color restore). - **Driving mode index shows what the playlist shows.** Each row has a 44 px round play / resume / replay button, the author ("by Name", smaller and muted, ellipsized), and the three-state listening indicator (not played / partly played / played) with accessible text, updated live as articles play and finish. Shares one state helper (`magListenState`) with the playlist. - **Article index in driving mode.** A new *Index* button in the top bar opens a sheet between the top bar and the controls dock that lists every article of the magazine with its title and length (m:ss or h:mm:ss; articles without audio are dimmed, show *no audio* and cannot be chosen). The playing article is highlighted and scrolled into view; tap or press Enter to jump to an article while driving mode keeps running. Escape, the close button or a tap outside closes it and returns focus to the button; Tab stays inside the sheet. Lengths come from the existing articles endpoint (`audio_seconds`), so there is no new backend call; a 113-article list is a plain list rendered at once. - **Share an article or a magazine as a link that opens straight in driving mode.** A *Share* button in the magazine toolbar (whole magazine), one in the playlist header and one in driving mode (current article) build `/#s-magazine?doc=<id>&article=<id>&mode=drive`. They use the browser's share sheet where there is one (it needs https, so on a plain-http LAN address they copy the link instead, with a polite aria-live toast, or show a box with the link if even copying is refused). Opening such a link, also on a first visit and also pasted into an already-open tab, opens the magazine and then driving mode for that article with audio, pictures and text; when the browser blocks autoplay a large *Tap to play* button appears. The link carries ids only (no key or password) and does not change who may reach the app; the ids are validated against a strict pattern and never put into markup, and anything malformed is ignored. Tested with Playwright (desktop deep link, blocked autoplay, hostile parameters, share sheet at 375 px, copy fallback). - **Filter the page grid by status.** Each legend entry is now a toggle button with the number of pages in that status; choose one or several to show only pages that have any of them. It combines with *Show only pages that need me*, the summary says how many pages are shown, *Show all pages* clears everything, a status no page has is greyed out, and the buttons announce their on/off state and work from the keyboard. - **Word-level timing with a forced-aligner sidecar.** A new, separate `tts-aligner` container (`aligner/`, torchaudio MMS_FA, CPU by default, optional `--profile gpu`; weights cached in the `aligner-models` volume, no published port, non-root, read-only filesystem, 3 GB limit) answers `POST /align` and `POST /align/batch` with `[{word, start, end}]`. The app (image unchanged, no torch) measures each sentence's words in a background job the first time an article is played (or via `POST .../articles/{id}/align-words`, progress via `GET`), writes `words: [[start_ms, end_ms], ...]` per segment into `audio/<id>.timings.json` (now version 2; readers of version 1 keep working) and `/timings` returns `words` per sentence. The player marks the word from those times in Compact, Lyrics (new per-word marking) and the playlist, and falls back to the previous length-based estimate when no word times exist or the sidecar is down. Measured on one article: after-pause word starts are within 29 ms on average (max 170 ms) of the audio's energy onsets. ### Changed - **Driving mode index rows are clearly separated.** Each row now has 13 px padding above and below and a thin divider line (the highlighted current row has none), replacing the bare gap, so multi-line titles with the play button, number, duration and status icon read as individual items. Hit areas stay 44 px and the 113-row list still renders at once. - **Driving mode index rows have more breathing room and lighter play buttons.** Rows now have 9 px vertical gap between them, staying tappable and scrollable. Each row's play/resume/replay button is now a light circle with a blue icon (matching the magazine playlist), styled with a surface background, subtle border and the app's primary blue for the icon, instead of a solid blue circle; the visual circle is 34 px with a 44 px accessible touch target maintained via padding. - **Driving mode: one display menu instead of the three-icon switch; fewer controls in the dock.** The top bar now has *Share*, a *menu* button (shows the current view), *Index* and *Exit*. The menu opens a flyout with *Picture*, *Lyrics* and *Compact* plus *Show text*, *Show pictures* (moved from the dock) and *Minimize text and picture*; it works with arrow keys, Home/End, Enter and Escape, closes on an outside tap, keeps 44 px targets, stays inside the screen from 320 px wide and never covers the dock. The playback speed button moved to the end of the slider row, after the time. The dock is now just the slider row and the transport buttons. Picture zoom stays on the picture. - **Driving mode has a new layout that always keeps the controls on screen.** The overlay is now one full-height column: a top bar (share, a *Picture / Lyrics / Compact* switch, minimize, close), a content area that shrinks and scrolls inside itself, and a controls dock pinned to the bottom (position slider, time, previous / -15 / play / +15 / next, speed, text, images). On an iPhone SE (375x667) the controls used to fall below the screen, the share button covered the article title and the top of the title was cut off; now the dock is always fully visible from 320x568 up to tablets, in landscape the picture and the three lines sit side by side, and long titles wrap to two lines. *Picture* fills the area (double-tap, pinch, wheel, +/- keys or the zoom buttons to zoom, drag to pan, Escape or the reset button to reset); *Lyrics* shows the whole text, the spoken sentence large and centered, tap a sentence to jump there, and scrolling by hand pauses following until *Back to current*; *Compact* (default) is the picture plus previous / current / next line. The minimize button shrinks everything to just the current line. The choice is remembered. Transport buttons stay perfect circles. - **Driving mode shows three lines of text instead of the whole article.** The previous sentence (small), the current one (large, with the spoken word marked) and the next one (small) sit in a fixed-height box, so the controls below never jump. Lines fade in smoothly (no animation when your device asks for reduced motion), the text is not a live region so a screen reader does not read out every sentence, it is readable at 375 px, and tapping the previous or next line jumps to that sentence. Pictures work as before. ### Fixed - **Driving mode lyrics view no longer leaves the previous line's word highlighted** when the playback moves to the next sentence. - **Sentence timings now also work for interview renders.** Every recorded audio file carries `"interview": true` (set whenever a second voice is configured, even without speaker labels), and the timings endpoint and EPUB export discarded all such records, so the player and the EPUB fell back to estimated sentence positions for every current article. The record is now matched to the text again, for interview renders by the label-stripped length the renderer recorded, so `/timings` returns exact (or anchored) sentence times and speech onsets for these articles too. Records for text edited since the render are still ignored. - **Character image runs are isolated per character.** The profile + reference-sheet job (`POST /api/character-generate-consistent-images`) could hand back an earlier character's job when the same `Idempotency-Key` was reused for a different character; the key is now bound to a fingerprint of the character's name, book, identity fields, style and prompts, and a mismatch is rejected with 409 instead of returning the other character's images. The fingerprint is not exposed in the status payload. New regression tests (`tests/test_character_image_isolation.py`) generate a dwarf and an elf back to back and concurrently and check that each ComfyUI request carries only its own prompts, props, identity image, seed family and result, and that the request data is not mutated. - **Characters table on phones: the rating stars can be tapped again.** At 320-414 px the pinned checkbox, picture and name columns (328 px) covered the stars of the voice rating; on phones they now scroll with the table, the rating column is wide enough for five 44 px stars, and each star has a *Rate n of 5* label. Browser tests check at 320, 375 and 414 px that no star touches a pinned column, is hit-testable, is at least 44 px and that the page itself does not scroll sideways; the phone case of the profile/rating test is no longer skipped. - **Highlight stays in step through seeking, speed changes and background tabs.** The highlighted sentence is now read from the audio's own position on every animation frame (and immediately on seeking, seeked, rate change, play, pause and when the tab becomes visible) using a binary search over the sentence start times, so there is no cached position to go stale; a jump into a silent gap keeps the sentence just spoken, and the marked word follows the sentence's own start and end, weighted by word length. New browser tests cover seeking back and forward, 1.5x and 2x, speed changes and rapid repeated seeks. - **Audio and text now line up when the voice actually starts.** Measured on real rendered articles (an 8-sentence and a 71-sentence MP3): the recorded sentence boundaries are exact (no drift over 10 minutes, no encoder-delay offset: the MP3 carries a LAME/Info header, so browsers trim the encoder delay), but the voice engine pads every sentence with 0.4 to 0.9 s of silence before the first word, so the highlight lit up about 0.7 s early. New renders now record where speech begins in each sentence (`onsets` in `<id>.timings.json`), older audio is measured from the decoded MP3 on request (in memory, nothing is written), and `/timings` returns it as `speech` per sentence. - **The item pictures on the character concept sheet show only objects, never hands, feet or people.** The lower-right tiles are now asked for as "a high-quality concept art prop sheet featuring isolated character accessories" on a plain, neutral studio background, still life, strictly inanimate objects, with no people, hands, faces or body parts; the item name (German names included) and the book's world and style stay in the prompt. A body-part list was also added to the item tile's negative prompt, which like the rest only acts if the cfg is raised above 1.0. Existing sheets need to be regenerated. - **Driving mode and the playlist highlight the sentence that is actually being spoken.** The player guessed where each sentence sits in the MP3 from its text length, so the highlighted line and the picture drifted from the voice (worst after seeking). The renderer already records the exact time of every sentence (`audio/<id>.timings.json`); a new endpoint `GET /api/magazine/{doc}/articles/{id}/timings` returns them (`{available, sentences: [{text, block, block_id, start, end}]}`, seconds, from the same sentence planner the EPUB uses) and the player uses them, falling back to the old estimate when an MP3 has no usable record (older audio, interview audio, text edited since). The transcript is still fetched with captions, but the MP3 is built without them, so a caption now gets no time slot (the estimate no longer spends audio time on captions either). The highlight is derived only from the audio's current time against the timeline, so the slider, skip buttons, tapping a sentence, lock-screen seek and resume all stay in sync. Covered by a backend test (`ArticleTimingsRouteTests`) and a Playwright test with seeking. - **Magazine player buttons stay round on a phone.** In the full-screen player at 375 px wide (iPhone SE) the five transport buttons were 52+76+110+76+52 px plus gaps, wider than the screen, so they shrank unevenly: the big play button became a tall oval and the 15-second skip buttons were squeezed. The buttons now have a fixed 1:1 size that scales down on narrow screens (play 64-84 px, others 44-56 px) and never shrink or stretch, and the player no longer clips its top or bottom when the content is taller than the screen. - **No gaps on the character concept sheet.** After the divider lines were removed, light cream gutters remained between the three poses and above the lower pictures, and the left pose's cape and the right pose's sword were clipped at them. The composer cut the generated triptych into thirds and pasted each into an inset panel (8 px per side), and the lower pictures had 12-18 px padding. The poses are now pasted as one whole image with no cut and no inset, and the lower pictures sit flush under it. The generation prompt never asked for gaps, so it is unchanged. Sheets already stored have the gaps baked in and need to be regenerated. - **No divider lines on the character concept sheet.** The Konzeptbild composer drew light lines between the three poses and between the top and bottom rows, which cut through capes and swords. They are gone. Sheets already stored have the lines baked into the image file and need to be regenerated to lose them. - **The two lower pictures of the concept sheet now show the character in the book's world.** For Marcian (a knight in plate and a red cloak, *Das Jahr des Greifen*) the story picture showed a modern man in a polo shirt with headphones and a camera in a garden, covered in pseudo-text captions, and the item tile a stock crossbow on a white backdrop. The story prompt was about 5,400 characters with the period rules at the very end, which the fast 4-step model never reaches; it opened with "ears clearly visible" and asked for "studio photography by a production photographer with a real actor", and words such as "unlettered", "overlays" and "Output 768x512" invited writing. The negative prompt that lists text and modern clothing had no effect, because these samplers run at cfg 1.0, where ComfyUI ignores it. The story prompt is now short and starts with the world (medieval high fantasy, The Dark Eye, 900-1300 AD, plus the book profile's setting, without the book title, which was read as lettering), then the same character as the turnaround (identity, appearance, costume, what the portrait shows), the moment from the book, a period background, and a hand-made, unmarked world; the item is the character's own, made in the colours and craftsmanship of their costume, on an oak table in a medieval room. Both are worded positively only; text, captions, headphones, cameras and modern clothing and buildings are added to the scene and item negative prompts for when the cfg is raised. Checked with two renders of Marcian on the local ComfyUI: a knight in plate and red cloak with a crossbow in a medieval village, and his crossbow on a table, with no text and nothing modern. Existing sheets need to be regenerated. - **Review opens at the top.** Opening a page in review no longer keeps the scroll position of the page grid, which could leave the sticky page bar covering the top of the page picture. --- ### Performance - **Big sections render lazily, so the page stays inside its DOM-node budget.** The Magazine section's markup is now fetched and injected the first time you open it (or deep-link into it) instead of at startup; the Performance background-jobs history is fetched and drawn only while Performance is shown (it used to add about 470 nodes at startup); and the language pickers keep a single option in the hidden native `<select>` while the searchable popup renders the full list on demand (setting `select.value` programmatically still works). Together this removes roughly 1,000 DOM nodes from the idle page and fixes the failing `tests/constrained.spec.js` node-budget test (5,862 nodes against a 5,000 limit before, about 4,750 now). Deep links, keyboard and screen-reader behavior are unchanged. ## [1.56.3] — 2026-09-30 ### Fixed - **Deleting a picture left an empty paragraph in its place (seen on c't p. 157 and 159).** A picture region is stored as two records, a visual block and an image entry linked by `source_block`, and the review list shows either one as a row. Delete removed only the row's own record, so the twin stayed at the same spot as an empty, unticked "Paragraph" that could not be told from a real one. Deleting either half now removes both (single delete, bulk delete and the type changes), the articles forget both ids, and Undo brings both back; a hand-drawn picture (no twin) and a text block that was retyped since are untouched. Copies of a magazine that already carry leftovers: *Clean up boxes* re-types an empty picture block that has no picture (not narrated, still listed, reversible; your production issue has 1 such, on page 158). Verified on your real page 159 through the real route on a scratch copy: delete removes both, Undo restores both. - **A turn in progress now says so.** While a turn is being read the picture and text on screen are still the old ones for 30 to 60 seconds, which looked like a failed turn (a screenshot showed the stale upside-down picture and its gibberish text beside "Re-reading page 156"). The review now shows a notice with the elapsed time saying the picture and text below are from before the turn, dims them and blocks clicks into them, and removes both when the read finishes. --- ## [1.56.2] — 2026-09-30 ### Fixed - **A turned page could keep showing its old picture, which made turning look broken (and invited pressing the buttons again).** Page pictures had no cache header until recently, and a browser may treat an unchanged URL as fresh for days, so after a successful turn the review could keep showing the old image. The layout now stamps every page with its picture's version (the file's modification time) and every page picture URL (review, page grid, reading pane, cover, lock-screen artwork) carries it, so a changed picture is a new URL in any browser. Turns also add up (right + half turn = three quarters), so the toolbar now says how far the page is turned from the scan ("Turned 270° clockwise from the scan") with an **Undo turns** button back to the PDF's own orientation, and a second turn or re-read of a page that is still being processed is refused (HTTP 409) instead of stacking. `extract_pages` records a stored turn in the page's layout entry, so it survives a re-import visibly. Verified end to end with the real OCR engines on a scratch copy of c't p. 156: turn 90 gives an upright 886x1039 page read at word-F1 0.878, Undo turns restores the 1039x886 original and clears the stored turn. - **A page's picture could vanish from the review (seen on c't p. 176).** One region held a 3,408-character token with no spaces (`Giro + 2.1.1.1.1…`, the OCR engine stuck in a loop). Its width was taken as the minimum width of the list column, which widened to 20,305 px and squeezed the page-picture column to 2 px. The review grid's tracks can now shrink (`minmax(0, …)`), and text in the review list, reading view, figure diffs and page cards wraps anywhere, so no content can collapse the layout again; a long URL or a loop now simply wraps. The cause is also handled: *Clean up boxes* (and every import and re-read, which run it) re-type a region that is mostly one endless repeated letter/digit token as OCR noise (kept, visible, reversible in the type picker, not read aloud). Leader dots, rules, section numbers, phone numbers, prices and URLs are not affected; run over all three of your magazines (about 6,000 regions) the rule flags exactly that one region on page 176 in each and nothing else. To clear it on an existing magazine press *Clean up boxes* or *Fix this page automatically*. --- ## [1.56.1] — 2026-09-30 ### Fixed - **The Draw region panel (and the "save new position" panel) could sit underneath the sticky Back / Approve bar and the status bar, cutting off Cancel / Add region.** The panel lived inside the page frame, which is in a sticky container; a sticky element is its own stacking context, so the panel's z-index only ranked it against the frame's content and the bars painted over it. Panels are now mounted on the page body (z-index 300: above the sticky bars, below dialogs), placed and followed as before, and cleaned up when the review redraws or is left. The region-type list opens upwards when there is no room below. The existing test only ever drew above the bar, so it could not see this; the new one draws where the panel lands over the bar and checks, with `elementFromPoint`, that the buttons are what a click would actually hit (verified to fail with the old layering). - **Turning a page no longer fails when the OCR engine cannot read the page in its new orientation.** On a real page (c't p. 156) the vision engine read the sideways image well (word-F1 0.878) but returned an empty answer for the turned page, so the turn was rolled back with "The OCR engine returned nothing usable". A turn now reads through a ladder: the turned page with the vision engines, then the page as scanned with its boxes carried into the turned page, then tesseract; the first plausible reading wins (on that page: the second, 13 regions, 168 words, F1 0.878), a picture page with no text can still be turned, and only if nothing can read it is the page put back. ### Added - **Turn a page that was scanned sideways or upside down.** *Turn left*, *Turn right* and *Turn 180°* in the review toolbar turn the page image, carry its boxes along so reviewed articles still match, and read the page again (`POST /api/magazine/{id}/pages/{page}/rotate`). The turn is remembered in the magazine's metadata and applied whenever pages are rendered, so re-importing does not undo it. If the re-read fails, the page goes back exactly as it was. Not covered by Undo (it changes the image): the opposite button turns it back. Measured on a real page scanned sideways: word-F1 0.016 as scanned, 0.876 after turning and re-reading. Page images now revalidate (`Cache-Control: no-cache`) so a turned page shows at once. ### Changed - **Fixing flagged words in the review list no longer restyles the paragraph or hides the underlines.** Clicking an underlined word now edits just that word in place (a small field in the paragraph's own type, suggestion chips, Enter applies, Esc cancels, *Keep* for names), through the same checked one-word fix as the spelling list. Clicking elsewhere in the text opens the whole-text editor, which now keeps the paragraph's font size, line height, width and wrapping (it was 13 px in a full-row box against 14 px text in a narrower column), grows with its text instead of scrolling in a small box, and keeps the doubtful words underlined while you type. On a phone the row text and editors are both 16 px (what iOS needs to avoid zooming in). --- ## [1.56.0] — 2026-09-30 ### Added - **OPDS catalogues: manage them in the app, log in, and page through results.** A *Your catalogues (OPDS)* panel adds and removes catalogues (validated server-side). A catalogue that needs a login takes a username and the *name* of an environment variable holding the password, which is never stored or shown and only sent to that catalogue's own host. Searches follow the catalogue's own next-page links (up to five result pages, same host only, loop-protected). - **Page overview says what to do.** Every page card now states its status in words, how many articles are on the page, where each article continues from / on to ("continues from p.3", "continues on p.6"), and a **To do** list when the system could not settle the page (approve it; fix the quality findings with the tool that fixes each; link orphan text to an article; generate audio; approve the other articles on a shared page). A summary line counts the pages that need you and *Show only pages that need me* hides the rest. The legend is larger (14 px, 18 px swatches). A page coloured "audio" that still has an unapproved article is now labelled "Partly done" with the reason. - **Hands-free listening.** The article player now registers with the browser's Media Session: title, magazine and first-page artwork on the lock screen / headset / car display, with play, pause, previous/next article, ±15 s and seek, plus a position bar. While driving mode is open and audio is playing the screen is kept awake (Screen Wake Lock), and released on pause or exit; a refused lock never affects playback. - **EPUB 3 with Media Overlays.** *EPUB with audio* in the contents toolbar (and `GET /api/magazine/{id}/epub`) packages the approved articles that have audio into a book whose text highlights as the audio plays. MP3 export now records each sentence's real start and end (`audio/<id>.timings.json`, no text), so new audio highlights exactly; older audio is placed by estimate and the book's description says so. Validated with W3C EPUBCheck 5.2.1 (0 errors, 0 warnings) on a real 12-minute two-article issue. - **Magazine reading view shows sidebars.** A sidebar was kept out of narration and therefore never shown at all. The article text endpoint now also returns `asides` and the reflowed view displays each one where it sits, labelled "Sidebar · not read aloud · p. N", as plain text that is neither a sentence nor highlighted. Only for pages the article alone occupies (on a shared page there is no telling which article a sidebar belongs to); not for advertisement pages; a sidebar you ticked into the narration stays narrated. - **Magazine review: extraction diff.** A region whose text you edited by hand is marked *Edited*, with *Show changes* (word-level comparison against the text as first recognised; removals struck through and marked "−", additions underlined and marked "+") and *Restore recognised text*. The first reading is kept in a new `ocr_text` field across repeated edits; re-reading the region or typing the original back clears it. - **"Continued from page N" now joins a feature's tail to its head.** Until now only the forward "continued on page N" reference linked pages. A tail that says "Fortsetzung von Seite 10" / "Continued from page 10" (found even when set as small furniture or sidebar text, on lines up to 160 characters) is attached to the single article on that page. Skipped when a contents entry anchors the tail, when the page is shared by several articles, or when the marker points forward or at an empty page. Articles gain a `continued_from` list. - **Magazine import from PNG/JPEG page images.** Choose or drop several scans or phone photos; they are checked by content (not by name), ordered by filename with natural numbering, turned into one scan-style PDF (page size from the image's own resolution, EXIF rotation applied, 72 dpi treated as unknown) and ingested like any scanned issue. Limits: 300 images, 25 MB and 60 megapixels each; a bad image rejects the whole upload and leaves nothing behind; an existing source is never replaced. Verified on real pages: OCR through the image path scored like the original PDF (F1 0.85 / 0.78). - `architecture/ocr-porting-prompt-general.md`: the OCR porting brief rewritten for general documents (letters, papers, forms, invoices). Adds privacy, pre-processing of skewed/photographed scans, document-type handling, field-level accuracy and handwriting, and tags every claim as measured on magazines, a general principle, or untested for these document types. ### Changed - **"Order by clicking" now keeps its controls on screen.** *Save order*, *Undo last* and *Cancel* lived in the toolbar row and could scroll out of reach, leaving the task unfinishable. While ordering they are a floating panel pinned to the bottom-right of the window (full width on a phone): a progress meter, a large *Save order* button that turns green when every box is numbered, *Undo last*, and *Cancel* (also **Esc**, except while typing in a field). It sits above the Back/Approve bar and clear of the page image. Tests scroll the real scroll container, and were verified to fail against the old layout. ### Fixed - **"Listen" on an article that already has audio did nothing until the native play button was pressed.** The player element is `preload="none"`, and starting a track waited for `loadedmetadata`, which such an element never fires. Choosing a track now sets `preload` so it loads and plays (and resumes at the saved position). - **MP3 export no longer throws away a finished synthesis when ID3 tagging is unavailable, and article durations no longer read 0 without mutagen.** The deployed image lacked `mutagen` although requirements.txt lists it, so tagging raised after every sentence had been rendered and the contents showed no MP3 lengths. Tagging is now best-effort (a warning), and durations fall back to ffprobe. Rebuild the image to get the tags back. - Docker: added an allow-list `.dockerignore`, so a build no longer uploads `config/` (user data, settings, keys), logs, tests or the git history into the build context; only the files the Dockerfile copies are sent. New image `tts-voice-creator-clone-and-design-2:latest` (also `:1.55.0`) includes `mutagen`; the previous image is kept as `:rollback-20260930`. --- ## [1.55.0] — 2026-09-29 ### Fixed - Tests: the form-pair fixture used the retired prompt version `v3` as "current"; the phone-width case of the Characters sorting test is marked known-failing (sticky name column covers the rating stars, tracked in TODO.md). - **Character scene images.** The scene prompt now states the character's gender and the book's pre-industrial period as hard constraints and asks for the focal person's face, upper body and hands in the near foreground. A sheet whose image checker is unavailable is no longer saved over a previously validated image: generation stops with a clear error instead. The weapon-integrity wording ("one hilt and one blade") is only added for characters who actually carry a weapon (it was planted into every scene). - The character-generation status poll rides out a transient 502/503/504 (proxy or app restart) instead of reporting a false "lost job". - **Voice benchmark failures now say why.** Designed voices are benchmarked through the Voice Design engine; when that service is down the row showed a bare "-" and the raw "Max retries exceeded" text. The error now names the service and the container to start, the Factor and WPM columns show `ERR` beside the time column, and a batch stops after three identical "unreachable" failures instead of retrying every remaining voice. ### Added - `architecture/ocr-porting-prompt.md`: a self-contained brief for carrying this app's OCR lessons (benchmark first, per-page escalation, fallback ladder, distrusting engine output, figure bounds from the render, box fitting, clean-up rules, quality audit, review UI) into another project. - `scripts/bench_ocr.py`: engine-agnostic OCR benchmark that scores an engine's transcription of rendered pages against a native-text PDF's own text layer (word F1, sequence similarity, seconds per page). First results (tesseract, Unlimited-OCR, docling, PaddleOCR) in `architecture/evidence/ocr-bench-2026-09-29/`: transcription accuracy is equal within noise (F1 0.865-0.881), tesseract is 4-20x faster; DeepSeek-OCR and Qwen3-VL-30B were not run (no memory headroom). - **Find an issue online (magazines).** A search panel in the magazine library finds issues in the Internet Archive and in any OPDS catalogue you configure (Calibre-Web, Kavita, Komga), shows the source's licence, and imports the PDF straight into a new magazine project, then starts ingestion. SSRF-safe (downloads only from the provider's own host), size/time bounded, feeds with DTDs refused. See `architecture/magazine-sources.md`. - **Magazine contents list shows synthesis state.** Each article row says "Synthesised · m:ss" (the MP3's exact length) or "Not synthesised" (approved, no MP3 yet), and has an inline play/pause button that streams the MP3 without opening the article. Icon plus words, screen-reader labelled, works at phone width. - `scripts/compare_magazines.py`: read-only comparison of two magazine folders (words, boxes, reading order, articles, quality numbers) for re-import checks. ### Changed (compare_magazines.py) - A page either side marks as a full-page advertisement is now excluded from every difference statistic and the worst-pages list, reported separately instead (`pages_skipped_as_advertisement`). Being marked an ad is a one-time manual step a fresh re-import has usually not had yet, so without this a page the baseline skips entirely (near-empty) was compared against the same page fully OCR'd in the new import -- a difference in review state, not pipeline quality, that was skewing the whole-issue numbers. ### Changed - Spelling agent and acronym resolver call the language model with thinking off (about 4 s instead of about 27 s per call) and retry without the option if the server rejects it. - The spelling agent treats run-together words (`dasich`) as errors and may split them at a space when both halves are real words and the letters are unchanged. - **A fresh import now finishes fully worked, not just laid out.** Ingestion runs the whole-issue Auto-fix pass (find missed text, re-read suspicious OCR, fix provable misspellings, link continuation pages, audit box coverage) itself as its last step, under the same job the ingest progress bar already follows -- no separate "Auto-fix all pages" click needed first. An Auto-fix failure is reported as an ingest failure (with the reason) rather than a silent gap; the ingest's own result (pages, articles) is kept either way. ### Fixed - Vision OCR: a refusal sentence the engine hallucinates over an unreadable photo or screenshot ("The image is too blurry to recognize...") is no longer narrated as page text -- discarded when the same sentence repeats across 3+ regions, confirmed live on c't KI Wissen 2026 pages 82, 87 and 124 (29 and 122 hallucinated regions dropped on re-OCR). - A duplicate box (its text already held by a wider box) that only becomes a duplicate once a later step in the same run grows or merges a box was left unmarked, so its text was narrated twice -- the duplicate check now also runs after the per-page box-fitting loop (ingest) and after finding missed text (the whole-issue quality run), not only before either. Confirmed live on 20 real pages across two runs of the same issue. ### Added - **Paragraphs are fused across columns and pages for narration.** A paragraph that continues into the next column or onto a later page is now one block in the reader and the exported MP3, so a sentence is no longer cut in two at the seam. Words split at the seam are closed up (`jähr-` + `lich`); real compound hyphens are kept; headings, interview turns and finished paragraphs are never glued together. Highlighting still follows the region each sentence began in. - **Pronunciation & spelling panel (per article).** Acronyms are spoken from the article's language (decided once per article, with per-block override for quotations) using an extended built-in list, letter-by-letter spelling for `USB`/`HDMI`-style acronyms, and an editable per-article glossary. Unfamiliar acronyms are listed with their sentence, and the language model can decide expansion / letters / word from context. - **German and English spellchecker with fix-or-mark.** Unknown words are underlined in the review list and listed with suggestions per page and per article; click a suggestion (undoable), or *Keep* to add the word to the issue's dictionary. *Fix with language model* corrects only what passes strict guards (a dictionary suggestion, or a known word within two edits) and keeps names/terms; the whole-magazine quality check now runs the same pass and reports pages with words still marked. - **Clean up boxes now also fits text boxes to their text.** A side that cuts through letters grows (at most a letter or two, never into a neighbouring box) and empty margins are trimmed. Boxes you drew or resized by hand are never touched. It also runs automatically after **OCR again (this page)**; on page 61 of the measured issue it widened the left-column paragraphs ~10 pt to cover their text, cutting clipped edges from 7 to 0. A box never grows into a neighbouring box; when it gains whole lines, its text is re-read from the enlarged crop and replaced only by a longer reading that still starts with the old text. - **Inset pull quotes get a region of their own.** A display-coloured quote set into the text (detection wraps body text around it, so no box covered it) is found from the page image — a muted-colour blob of quote size on plain paper that no headline, figure or existing pull quote covers — and added as a *pull quote* region (never read aloud). Runs after **OCR again (this page)** and in **Clean up boxes**. On the measured 204-page issue it finds the two real quotes (pages 40 and 62) and one infographic (page 139, harmless and deletable). - **A paragraph that wraps around an inset pull quote is cut into a full-width part and a narrow part**, so its box no longer sits over the quote (as the right-hand column already did). The text lines are measured from the page image; the text is cut in proportion to line width at a space, within a word or two of the printed break, and the two pieces are read in order and rejoined for narration. A paragraph whose lines are all narrow just gets a narrower box. Runs after **OCR again (this page)** and in **Clean up boxes**; new pieces join the same articles. - **Review shows what is wrong with each page, on the page.** A *What is wrong on this page* panel (measured live from the page render, not from the last run) explains each problem in plain language with its own fix button, and marks it on the boxes: red dashed frame = text outside any box, orange edge = a box edge cutting through text, purple dashed = two boxes over the same text, yellow dotted = words that look mis-read, red wavy underline = unknown words. **Fix this page automatically** fits boxes, adds inset pull quotes, cuts paragraphs that wrap around them, finds missed text, re-reads suspicious words and (with a language model) fixes provable misspellings, with one Undo. - **The whole-magazine run now fixes as well as checks:** *Quality check / run* also fits boxes, finds inset pull quotes and cuts wrapped paragraphs on every page before the missed-text, re-read and spelling steps, and reports how many boxes it fixed. - **Text flow indicator.** Review draws a green arrow where an article's text carries on to the next box below or the next column, and chips where it continues on another page (*continues on page 111 ›*, *‹ from page 110*) plus *article starts / ends*; a *Show text flow* switch hides them. The reading text shows small markers (`↓`, `col ›`, `p.111 ›`) at the exact word where a paragraph crosses a box, column or page. Built from the same fusion the narration uses (`GET .../articles/{id}/flow`), so the arrows show what is read as one paragraph. On the measured issue: 97 column, 29 page and 21 below continuations. - **The whole-magazine button is now named *Auto-fix all pages*** (it fixes boxes, adds pull quotes, finds missed text, re-reads words and fixes provable misspellings on every page) and keeps a layout backup first. - **Continuation pages are linked into their articles automatically.** Detection often found an article's headline page and left the pages that follow as one-page stand-ins ("Page 61") that had to be approved one by one (79 of 132 articles on the measured issue). Text no article owns is now attached to the article running from the page before, unless that article already ended (a signed end such as `(hag)`), it is more than two pages back, or a headline with a byline/deck opens a new article there. Text you already approved (as part of a stand-in page) stays approved when it is merged; an approved article that gains text nobody approved goes back to *detected*, so nothing unreviewed is ever narrated. Stand-ins whose text moved are removed. Runs at import, in **Auto-fix all pages**, and on its own (`POST .../articles/auto-link`, undoable). - **Link an article to a page by hand:** *Link to page N — Article continues there / starts there* in the review panel picks the text that belongs (the paragraph that carries the sentence on, up to the next headline) and adds it ticked; undoable. - **Flow arrows follow the reading order:** a green arrow for every jump to another column and for a sentence running across boxes (dashed when a new paragraph starts, solid when a sentence continues), chips for page jumps. - **Much less noise:** the *Auto-fix all pages* report now lists what was fixed and only the pages that need a decision about their boxes (with a *Start with page N* button and compact page pills); pages with only words to check are collapsed and optional. The per-box "words look mis-read" marks are gone (spelling keeps its red wavy underline). The spellchecker knows ~130 company/product names, treats a word printed twice anywhere in the issue as vocabulary (missing-umlaut words such as `fur` stay flagged) and treats two unknown capitalised words in a row as a person's name: 871 → 625 findings on the measured issue. - **"Article ends" is only claimed when it is true.** An article counts as ended when it is signed off (`(rah) et`, `(hag)` — OCR noise after the code is tolerated) or when the next region, on the same page or at the top of the next, is a headline, deck or byline. Otherwise the last box shows a clickable *no end marker — link page N ›* chip that adds the next page's text in one click. - **Photos and graphics that no box covers get a figure region automatically** (a dense unboxed blob of real size; thin, line-structured ink is text and is left alone). New figures join the article with the most text on their page. - **Text that is not narratable page furniture stays out of linked articles:** running heads that recur on 3+ pages, lists of page numbers and reference/source lists are no longer pulled in when continuation pages are linked. An article made only of text you already approved becomes approved. - The per-page fix and the whole-issue run keep `quality_report.json` in step, so the thumbnail badges and the list of pages needing a decision match the layout as it is now. Running heads and adverts are no longer audited for clipped edges or overlaps. - **Articles nobody has reviewed are blue.** In the article list an unapproved, unrejected article has a blue background and edge and a *not reviewed* pill (approved keeps its green edge), so what still needs a person stands out. - **Overlapping boxes are fixed automatically.** Where a lower-ranked box runs over a higher-ranked one (headline > deck/byline > body > caption/sidebar/pull quote), the lower one is cut back to the largest part that no longer overlaps, unless that would leave under 40% of it (then both stay for a human). Hand-placed boxes and boxes of equal rank are never touched. - **Fewer false alarms and better fits:** a strip of solid ink beside a headline (a photo or filled panel behind it) is no longer counted as letters cut off; headline and deck boxes may grow up to 70 px sideways (body boxes 16 px); an inset pull-quote box that repeats text a box already holds becomes a duplicate. - **Lines and frames are not missed text.** Beyond dotted leaders and thin rules, the audit now tells letters from lines by shape: text is many small separate marks, whereas a screenshot or chart frame, a vertical rule, a chart axis or a signature is a few large connected shapes (long and thin, or big and hollow). Only such shapes are ignored; a paragraph of text inside a frame, and a solid photo, are still found. Unboxed areas on the measured issue fell from 105 to 27. - **Rules and dotted leaders are not missed text.** The dots between a table-of-contents entry and its page number (and thin printed rules) are ink but no letters; the audit now ignores ink that is only a few pixel rows tall. - **Stacked boxes of a column now meet at the gap between two lines** (*seam mending*). Detection often cuts a column into slices that overlap or leave a line in the seam, so that line was in neither box and both reported a clipped edge. The shared boundary moves to the nearest blank gap (below the line for a paragraph that is still running, above it after a finished one, in the middle of an overlap); a line that had fallen into the seam is read on its own and attached to the box that now owns it, and the box takes the column's width. Page 32 of the measured issue: clipped edges 2 → 0 and the lost line "Entwicklungen auseinander. „KI ist bei uns in allen" is back in the text. 57 seams were mended across the issue. - **"Edge cuts text" needs the box's own lines.** Ink beside a left/right edge only counts as cut-off letters when it sits on the rows where the box itself has text and is intermittent (letters and gaps); a solid vertical line (a screenshot frame, a chart border, a photo edge) or ink between the box's lines does not, and a thin horizontal rule above or below is not a cut line. On the measured issue clipped edges fell from 82 to 48 and pages needing a decision from 53 to 42. - **Unboxed ink in or beside a picture, chart or advert is no longer reported as missed text** (labels of a chart, text set into a photo, an advert's own wording). - **Undo and Redo buttons in the sticky review bar**, at the left end of its second row (placed by measuring the bar, so they stay there at any window width), also on Ctrl+Z and Ctrl+Shift+Z / Ctrl+Y (not while typing in a text field, where the browser undoes the typing). The last 20 recorded changes are kept (compressed, in the magazine's `undo/` folder) and each button's tooltip names the change it would undo or redo. More changes are now recorded: approving/rejecting or editing an article, moving or resizing a region, drawing a region, re-reading a page, setting or releasing a page's reading order, *Fix reading order* and *Clean up boxes*, on top of region edits, linking, auto-fix and spelling corrections. A new change ends the redo history. As before, an undo (or redo) is refused if something else changed the magazine in between, so it can never roll back work it does not know about (`GET /history`, `POST /undo`, `POST /redo`). ### Changed - **The Contents button is the first item of the sticky review bar**, so it stays in reach while scrolling a long page. - **The page thumbnail is the Review button.** In the thumbnail grid, clicking the (big) page image opens the page in review; the separate *Review* / *Review this page* buttons are gone. A page shared by two or more articles keeps one labelled *Review* button per article, so it stays clear which is meant. - **Click a flagged word to correct it yourself.** In the spelling list, clicking the underlined word (or *Edit…*) turns the row into a small editor prefilled with the word: type the correction — one word or several, e.g. `dasich` → `das ich` — and press Enter (Escape cancels). Until now only the listed suggestions could be applied. The server accepts any short single-line text without markup for a correction you type; model answers still go through the strict check. ### Fixed - **The new-box popup is no longer cut off.** Drawing a box near the bottom of a page put the *Region type / Add region* panel below the window edge. It is now placed in the window: below the box when it fits, above it otherwise, always fully inside, and it follows the box when the page scrolls. - **Changed scripts and stylesheets now reach the browser immediately.** They were requested under a version that only moved at release time (`…js?v=1.53.4`) and cached as immutable for a year, so a browser or proxy could keep running old code after an update (a button added to a script simply did not appear). The app shell now requests every asset under `<version>-<build id>`, where the build id follows the newest script/stylesheet/manifest change. - **An approved page is no longer purple.** A page the quality check flagged stayed purple even after you approved its article; now your approval wins and the page shows its review colour (the warning badge on the card still lists what the check found). Purple means flagged *and* nobody has approved anything on the page. An old MP3 on an article nobody approved (left over from before pages were linked) no longer turns a page green either: audio only counts for approved articles. - **Reading order is now applied automatically after import, *OCR again* and every per-page fix**, not only by the manual *Fix reading order* button. Until now the order after import was whatever the vision/LLM step returned, so a column's short last box could be read after the whole other column; on the measured issue 101 of 204 pages had a saved order that differs from the column-aware order. Pages whose order you fixed by hand stay untouched, except after *OCR again*, when every region is new and a hand order can no longer apply. - **The review list shows each region's whole text**, not the first 220 characters, so every misspelling gets its red wavy underline (like a word processor). The engine's `[Non-Text]` marker and hairline boxes (under 3 pt tall) are now treated as OCR noise by *Clean up boxes* and after a page re-OCR, so they no longer show up as near-invisible regions. Fitted boxes keep a little air on grown sides. - **The quality check no longer flags correct text as "suspect OCR".** The word check guessed the language per fragment (`iPhones statt Token` read as English) and flagged valid German; it now uses the page's language, the same lenient German rules as the spellchecker, and treats words that recur on the page (names, brands) as vocabulary. Page 61 of the measured issue went from 11 suspect regions to 3 (one a genuinely truncated word), and the issue from 758 to 610 before the language fix's full effect. - Suspended compounds (`Programmier- und Hausaufgaben`) no longer lose their hyphen when regions are merged or layouts are grouped; already-ingested `Programmierund` shows in the spelling list with a one-click repair. ### Changed - `prepare_text_for_tts` accepts the article language and glossary; narration no longer guesses the language per fragment. --- ## [1.54.0] — 2026-09-27 ### Fixed - **Phone review layout:** the review toolbar no longer sticks to the top on narrow screens, where it covered most of the page image and made drawing regions or drag-selecting boxes start on a toolbar button. - **The region-type picker in review opens again** and **"Find missed text" / "Verify OCR text" work**: their click handlers were never exported to the page. A new unit test fails if any inline `mag*` handler is not exported. ### Added - The quality check now measures box edges at pixel level on all four sides and reports articles that claim the same text regions (duplicates that would be narrated more than once). - **Driving mode is now a podcast-style player.** Back/forward 15 seconds, a seek bar, a playback-speed button (0.75×–2×, remembered), and toggles to show or hide the article text as karaoke (whole article scrolls, current sentence and word highlighted) and the article's images. - **Full-page advertisements are recognised automatically.** A page with detected advertisement regions and no narratable text turns green (dashed), is never read aloud and is skipped by the quality check — no button or manual approval. (`PUT/DELETE /api/magazine/{id}/pages/{page}/advertisement` still exist to force the state.) - **Whole-magazine quality check.** The "Quality check / run" button (first in the contents toolbar) runs, per page: missed-text recovery, guarded OCR re-reading, and a pixel audit that reports ink outside every box, overlapping text boxes, and boxes clipping the first/last letters of a column. Pages that still need attention are listed with links into review; the report is saved as `quality_report.json`. Thumbnail cards gain quality-stage colours (clean = blue, problems = purple overriding review colours, with a clickable warning badge on the thumbnail listing what was found) and a live per-page indicator while the run works. The same indicator now covers every batch step (OCR/ingest, paragraph and advertisement recognition, figure fixing, audio rendering); each background job reports `{page|pages, step}` in its result. --- ## [1.53.5] — 2026-09-25 ### Added - **Magazine review now has two conservative OCR quality checks.** “Find missed text” uses an independent local pass to add only genuinely absent, non-overlapping regions; “Verify OCR text” uses language-aware spell checking only to flag suspicious regions and changes text only after a guarded re-read of the real page crop. - **Whole-magazine figure-boundary repair is available as an explicit batch action** in the contents view, while the review-page action is now page-scoped. ### Changed - **Magazine TTS expands known abbreviations without changing displayed OCR text.** In German, `KI` remains visible as printed but is spoken as “Künstliche Intelligenz.” - **Figure crops use pixel-level edge refinement for dense images.** Clear image boundaries are tight on all four sides instead of retaining coarse-grid paper margins on only some edges. ### Fixed - **Approve & Next advances to the immediately following physical page.** Page navigation also switches to the article that owns the visible page (or creates a page-scoped placeholder), preventing multi-page articles from being skipped and preventing approval from mutating the wrong article. - **Drop caps no longer introduce spaces or lose an entire opening line.** Split initials such as `S prechende`, `K I`, `A m`, and `B i s` are joined, and guarded second reads restore article-opening text even when the surviving remainder begins with a valid capitalized word. - **Semantic layout no longer creates giant paragraph boxes across two columns**, reorders run-on byline fragments, or labels ordinary lower-page paragraphs as captions when no figure is nearby. - **Narrow pull quotes between columns remain separate regions**, and figure/advertisement cleanup is less likely to absorb surrounding editorial text. - **All eight resize handles now have the same appearance.** The delete control no longer covers the north-east resize handle. --- ## [1.53.4] — 2026-09-24 ### Fixed - **Playwright's iPhone project can launch Chromium reliably.** The iPhone 13 device profile defaults to WebKit, but the shared system-browser override supplied a Chromium executable, causing Playwright to pass WebKit launch arguments to Chromium and lose the browser immediately. The mobile project now explicitly selects Chromium while retaining iPhone viewport, touch, scale-factor and user-agent emulation. --- ## [1.53.3] — 2026-09-24 ### Fixed - **Magazine: paragraphs flowing from one column into the next no longer create a giant union box over both columns.** The semantic stage keeps one physical box per column while still merging fragments within each individual column. Existing saved union boxes need “OCR again (this page)” or a fresh import because their original component boxes have already been discarded. --- ## [1.53.2] — 2026-09-24 ### Added - **Magazine: exported MP3s can use a second voice for printed interviews.** When exactly two recurring speaker labels are detected, the narrator and optional interview voice alternate turns without reading the labels aloud; ordinary prose containing colons is left alone. - **Magazine: generated-audio playlists now include a synchronized reading companion.** The playlist shows page, duration and played/partly-played/unplayed status, follows the current sentence and word using estimated timing, displays the article's figures alongside the transcript, remembers playback progress, and offers previous/play/next controls plus a progress display in driving mode. - **Magazine: title and issue metadata can be edited after import.** Exported filenames use a parsed issue year/month when available, and a new “Regenerate all MP3s” action rebuilds approved audio after metadata, text, pronunciation or voice changes. - **Magazine review: a page-level checkbox selects or clears every text and image region**, including an indeterminate state for partially selected pages. ### Changed - **Magazine narration now performs conservative OCR and pronunciation cleanup without changing displayed source text.** Printed line-break hyphens are removed, known German/English abbreviations are expanded for speech, and dictionary-backed missing German umlauts are restored only when unambiguous. - **Magazine page cards now show every article sharing the page with direct Review and Play actions**, and the article list is once again the default contents view. - **Voice searches now include display names and tags** in both the voice library and shared voice pickers. ### Fixed - **Magazine: oversized drop caps no longer make vision OCR omit the first line of a paragraph.** Suspicious lowercase starts receive a guarded local-OCR second opinion that is accepted only when it substantially matches the existing transcription. - **Magazine: internal headings on continuation pages no longer split a multi-page article into separate playlist items.** --- ## [1.53.0] — 2026-09-23 ### Added - **Magazine: the "All pages" grid is colour-coded by review status** — blank (no article yet, the normal mid-review state), red (has content, not reviewed), yellow (reviewed), green (reviewed and MP3 generated), grey (reviewed, nothing to narrate — a blank page or an ad). A small legend explains the colours. ### Fixed - **Magazine: "Review this page" on a page-coverage gap failed with "Method Not Allowed."** The endpoint it needs (`POST /api/magazine/{doc_id}/articles`, added alongside the gap-jump feature itself) had never actually been deployed with a container restart. --- ## [1.52.1] — 2026-09-22 ### Fixed - **Magazine: the page-grid column slider only changed card width, not thumbnail size.** A fixed thumbnail height meant fewer columns just left more empty space around the same small image instead of showing it bigger. The thumbnail now sizes by aspect ratio instead of a fixed height, so the slider is a real zoom. ### Removed - **Magazine: the page-coverage gap banner.** Redundant now that the "All pages" view's own per-card "no article" tag already says the same thing in context — it was a second warning repeating information already on screen. --- ## [1.52.0] — 2026-09-22 ### Changed - **Magazine: the article-thumbnail grid is gone; "Thumbnails" now means the page-by-page view.** An article is a poor thumbnail unit — it can span many pages or share one with others — so the toggle is back to two states: List (articles, for the title/author/duration/status detail that only makes sense per article) and the page-complete thumbnail grid added earlier. "All pages" is also now the default view on first open. - **Magazine: the page grid's column count is adjustable, 1-12**, via a slider next to the view toggle (shown only in that view) — a "zoom" between an overview of the whole issue and a close look at a few pages, remembered per browser. --- ## [1.51.0] — 2026-09-22 ### Fixed - **Magazine: reviewing a page-coverage gap borrowed an unrelated article as context.** Approve or "Not an article" on that page would have silently changed the status of whatever nearby article was borrowed, not said anything about the page actually being looked at. A gap page now gets its own dedicated article record (`POST /api/magazine/{doc_id}/articles`) instead. - **Magazine: "Approve" (now "Approve & Next") jumps to the next unresolved page, not the next article.** Advancing by article order alone was exactly what left page-coverage gaps invisible to this workflow in the first place; it now walks forward page by page and stops at whichever needs a decision next — an unreviewed article, or a page nothing covers yet — reusing the same one-click flow as the coverage-gap banner either way. --- ## [1.50.0] — 2026-09-22 ### Added - **Magazine: an "All pages" view.** Articles are a poor unit for "have I seen everything" — one can span many pages, several can share one page, and neither the list nor thumbnail view (both article-grained) can show either case. This adds a third view, cycled by the same toggle button, showing every page 1..N with its own thumbnail and whichever article(s) claim it (a page shared by two pieces shows both; a page claimed by none is flagged and links straight into Review there via the page-coverage jump added earlier). Articles remain the unit for narration, Approve, and audio export, which are inherently article-scoped — this is a complete-picture view alongside them, not a replacement. --- ## [1.49.0] — 2026-09-22 ### Added - **Magazine: a page-coverage gap finder.** Approve only ever advances to the next detected article, so a page never grouped into any article at all — an ad, a missed piece, anything detection skipped entirely — was never surfaced by that workflow. Confirmed live: 73 of 204 pages (36%) in one real issue belonged to no article whatsoever. The article list now shows a banner listing every such gap as page ranges, each a one-click jump straight into Review at that page (the review UI's own page navigation already isn't restricted to one article's pages — the missing piece was purely that nothing pointed there). --- ## [1.48.2] — 2026-09-22 ### Changed - **Magazine: "Generate all audio" progress now shows page number, not article count.** "Article 6/44" said little about how far through the magazine a run actually was; it now maps back to that article's own starting page and shows "p. 20 / 204" instead, falling back to the article count if a page number isn't available for some reason. --- ## [1.48.1] — 2026-09-22 ### Fixed - **Magazine: "Listen"/"Continue" on an article with a generated MP3 still went through the live reader.** It hit "Pick a narrator voice first" instead of just playing the file that already existed. Both buttons now check for a generated MP3 first and play it instantly through the playlist player — no voice pick, no synthesis wait; "Continue" resumes from the saved position when one exists. Only an article with no generated audio falls back to the live sentence-by-sentence reader. --- ## [1.48.0] — 2026-09-22 ### Added - **Magazine: "Generate all audio" resumes instead of redoing everything.** It now only renders approved articles that don't already have an MP3 — safe to click again after an aborted or partial run (a container restart mid-batch, closing the tab, etc.) without re-synthesising what already finished. --- ## [1.47.1] — 2026-09-22 ### Added - **Magazine: a progress bar for "Generate all audio."** The export status was a single line of text that jumped between "Article N/Total…" and a mid-render "sentence X/Y" — now a `<progress>` bar plus a plain "N / Total" count track the same numbers visually, parsed from the job's own progress text. --- ## [1.47.0] — 2026-09-22 ### Added - **Magazine: click a region's text to edit it, no pencil icon needed.** The pencil-icon editor (with its Save/Split-here/Cancel actions) still opens exactly the same way — this just adds the text itself as a faster path to it, since the row is a checkbox `<label>` and a plain click would otherwise just toggle the tick. - **Magazine: review boxes are colour-coded by region type**, reusing the exact palette the live ingest preview already used — a headline, an author line, an advertisement now each have a distinct colour on the page itself, not only while detection is running. A small legend explains the colours; excluded/unticked and cross-article-linked boxes keep their own distinct look regardless of type. --- ## [1.46.0] — 2026-09-22 ### Added - **Magazine: readable filenames for exported MP3s.** A downloaded or zipped article MP3 is now named `Magazine Title - p<page> - Article Title - Author.mp3` instead of its internal id; the on-disk file itself is unchanged (still the stable id), only the name offered on download/inside the zip. The zip's entries are also numbered by reading order, so extracting and sorting by filename plays back in the magazine's own order. - **Magazine: an MP3 playlist.** A new Playlist panel lists every article with a generated MP3, in reading order, with cover art, title/author and a player that auto-advances to the next track. "Play all articles" now uses this directly (no synthesis wait) whenever every approved article already has a generated MP3; a queue with even one ungenerated article keeps the original live sentence-by-sentence reader, so a listener never hits a silent gap mid-drive. --- ## [1.45.0] — 2026-09-22 ### Added - **Magazine: Title/Author auto-fill from a Headline or Author region.** A newly drawn Headline or Author region, or one retyped from the picker, now fills the article's Title/Author field automatically instead of requiring "Pick from page" by hand — only when that field is still empty, so it never overwrites something already typed or picked. --- ## [1.44.0] — 2026-09-22 ### Added - **Magazine: Draw region stays armed after adding a box.** Previously every box meant clicking the toolbar button again; it's now a toggle (with an on/off switch icon on the button itself, since the background colour alone was easy to miss after a few boxes in a row) that stays active until you turn it off. - **Magazine: a freshly drawn region is ticked in automatically** when its type is one that's actually narrated (Paragraph, Headline, Subheading, Standfirst, Author) — drawing a region already means it belongs in the article. Furniture-like kinds (Page number, Source/footer, Advertisement, Table, Duplicate, OCR noise, Caption, Pull quote, Sidenote, Text in picture) stay unticked, since those are excluded from narration on purpose. - **Magazine: the region type list is now searchable, with an icon per type.** Filter any of the 16 kinds by typing instead of scrolling a plain dropdown. ## [1.43.1] — 2026-09-22 ### Fixed - **Front-end changes could appear to never take effect, no matter how hard the page was reloaded.** `static/index.html` hardcodes `?v=` cache-busting query strings on `style.css`, the per-page CSS files and `loader.js`, plus an `<meta name="app-version">` tag — nothing kept these in sync with `VERSION`, so they had silently drifted to different stale versions each (found live: `style.css` one release behind, `loader.js` sixteen releases behind). The server's static-file middleware marks any CSS/JS request *with* a `?v=` query as `Cache-Control: public, max-age=31536000, immutable` — a reasonable rule for genuinely fingerprinted assets, but since these tags never changed, a browser that had ever loaded e.g. `magazine.js?v=1.26.10` kept serving that exact response for a year, regardless of how many times the file changed on disk or how hard the page was force-reloaded. `scripts/release.py` now rewrites every `?v=X.Y.Z` and the `app-version` meta tag to the new version on every release, so a release is what actually invalidates the cache — not a manual, easy-to-forget edit. ## [1.43.0] — 2026-09-22 ### Added - **Magazine: one "Draw region" tool instead of six.** Draw text, Draw image, Headline, Page number, Source/footer and Advertisement were six separate buttons that each pre-selected a region kind before you'd even drawn a box. There is now a single Draw region tool; the panel that appears after you draw the rectangle has a Region type dropdown covering every kind (Paragraph, Headline, Subheading, Standfirst, Author, Caption, Pull quote, Sidenote, Page number, Source/footer, Advertisement, Text in picture, OCR noise, Duplicate, Table, Image) — pick what it is after you've seen the shape, not before. - **Magazine: "Table" is a real region type.** Marking a region as a table keeps its OCR'd text visible for reference but excludes it from narration, the same as other non-body furniture — no more a dense grid of numbers being read aloud as a flat word stream. - **Magazine: every text-like drawn or re-read region auto-OCRs when left blank.** Previously only a handful of preset kinds triggered automatic reading; now the rule is uniform — every text-like kind reads itself in, and only Advertisement and Image are skipped (nothing to transcribe in either). - **Magazine: thumbnail grid view for the article list**, toggleable alongside the existing list view, remembered per browser. - **Magazine: batch approve/reject.** Select several articles at once and approve or reject them in one action — accepts each exactly as auto-detected, for the pieces that already look right; a messy one still needs individual Review. - **Magazine: inline title editing.** Click an article's title, in either view, and type — fixes a mangled OCR title ("Sbimmb das? c'b prüFb") without opening Review. - **Magazine: standalone MP3 export.** Render any approved article to a single MP3 tagged with title, author, album (the magazine's title), its own text as embedded lyrics, and its page image as cover art — for a phone's own music/podcast player or a car head unit, not just this app. "Generate all audio" renders every approved article in one background job; per-article downloads plus a "Download all" ZIP. - **Magazine: "Remove all boxes."** Deletes every detected region on the current page in one action, with confirmation and Undo, for when the auto-detected layout is too tangled to fix box by box and it's faster to start the page from nothing. - **Magazine: Approve moves straight to the next article** for review, instead of dropping back to the list — reviewing a long queue no longer means re-opening each one by hand. ### Fixed - **Magazine: resizing an image region's box didn't visibly update the crop.** The server already re-crops the file correctly, but writes it under the same filename every time, so the browser kept serving its cached copy of the old crop. The preview now busts the cache on the region's own bbox, which changes exactly when the crop does. - **Magazine: a region drawn or re-read while the vision OCR backend was unreachable always came back "no text recognised."** Confirmed live: the configured vision OCR endpoints were refusing connections (a GPU memory conflict on the host). Both the new-region and re-read paths now fall back to the local Tesseract engine instead of giving up outright. - **Magazine: the review toolbar no longer fits or stays put on a long page.** Its two page steppers (one bounded to the current article's own pages, one to the whole magazine) almost always agreed and just doubled the toolbar's width; merged into one. The toolbar is now sticky so it stays visible while scrolling a long page's regions. - **Characters: several recurring portrait-rendering biases fixed**, each confirmed live across this session's cast for the DSA audiobook series: hair defaulting to a modern barbershop cut, or outright bald/receding, regardless of what the sheet said about coverage; an armored woman rendering with a shaved head and masculinized features; a duplicated or malformed weapon (a second sword shown alongside the drawn one, a two-handed Zweihänder rendered with two separate hilts); a crossbow or bow rendering as a modern firearm, traced to the ranged-weapon detection regex missing German compound words like "Armbrustbolzen"; an established dwarf still rendered with tall human body proportions; and a scene panel occasionally adding an extra hand or losing an established non-human species. ### Changed - Added `mutagen` to `requirements.txt` for the new MP3 export's ID3 tagging (title/artist/album, embedded lyrics, cover art). ## [1.42.4] — 2026-09-22 ### Fixed - **Magazine ingestion: a proposed picture, chart or table sometimes wasn't one.** The semantic-layout pass can invent a new picture region for a graphic OCR missed; on a real issue it sometimes drew that box over a column gap and the text in it instead (a table-of-contents heading and intro paragraph in one case), burying real paragraphs under an empty "Figure" placeholder. A proposed region is now checked against the page render's own ink density before it is created — a real photo, chart or table is solidly inked, a text area is not — confirmed against the actual false positives and true positives found on that issue (0.10-0.14 vs 0.56-0.97 mean ink, no overlap). QA also spot-checked headline, paragraph, column, page-number and advertisement recognition across a sample of pages of that issue; no other errors found. ## [1.42.3] — 2026-09-22 ### Fixed - **Magazine import preview: picture, chart and table boxes were drawn in the wrong place, and the recognised-text panel never showed them.** Both came from the same cause: the boxes were only snapped to the real picture edges and cropped to actual files once, after the whole magazine had finished importing — during the import itself the preview drew the vision model's raw, often-imprecise guess, and the text panel could only show a generic "Figure" placeholder because no crop file existed yet. Pictures, charts and tables are now snapped and cropped as soon as each page is labelled, so the live preview's boxes are accurate and the recognised-text panel shows the actual picture instead of a placeholder, while the import is still running. ## [1.42.2] — 2026-09-22 ### Fixed - **Magazine import preview: pictures, charts and tables were hard to see.** On a scanned magazine every page is one embedded picture, so the preview framed the whole page as a "Picture" in the same orange as the real pictures on it. The page scan is no longer framed, pictures, charts, tables and code each get their own colour as dashed, filled boxes, the legend lists each label once, and during the first pass the legend says that pictures are marked in the second pass. ## [1.42.1] — 2026-09-22 ### Fixed - **Magazine import preview: the page sat at the bottom of its pane.** The page image (and the region boxes drawn over it) is now aligned to the top of the preview, so it stays in view next to a long recognised-text column. ## [1.42.0] — 2026-09-22 ### Added - **Magazine import: watch the regions being found.** The live page preview draws every region over the page as it is recognised — coloured by type, numbered in reading order, with a legend — and follows the second pass (*Recognising paragraphs, headings and advertisements*) page by page, so you can see boxes change type; the progress line shows which page it is on. ### Fixed - **An interrupted magazine import spun for ever.** When a server restart killed the import job, the magazine stayed "running" and the page kept showing "Recognising paragraphs…"; it now reports the interruption so the import can be started again. - The live preview wrote nothing during the second pass (its regions were in a format the text preview could not read). ## [1.41.1] — 2026-09-22 ### Fixed - **Magazine: "Could not add region: … unexpected keyword argument 'original_text'".** After *Clean up boxes* split an author line, drawing a region (and re-OCR of a page) crashed on the extra field kept with that author line. The field is now part of the region model and survives every path. - **Undo could roll back more than the change it belonged to.** An Undo offered for a delete stayed valid while other changes that record no undo point of their own (drawing a region, fixing the reading order, clean-up, approving) happened afterwards — pressing it would have reverted those too. Undo now refuses as soon as anything else has changed, and those actions clear the Undo button. ## [1.41.0] — 2026-09-21 ### Changed - **The character sheet follows the finished profile picture.** After the portrait is made and checked, what it shows (hair colour, length and style, facial hair, eye colour) is written into the turnaround and story-moment prompts, so a character whose sheet says nothing about hair no longer gets long hair in the portrait and a crop on the sheet. ### Fixed - **Profile portraits in a forest instead of the studio.** The portrait prompt is now short and framing-first as well (like the turnaround in 1.40.0): measured on a character that got scenery in 4 of 4 portraits, 4 of 4 now have the plain studio backdrop. - Armed characters again get their own costume cut and colours in the sheet (lost when the turnaround prompt was shortened). ## [1.40.0] — 2026-09-21 ### Fixed - **Character sheets: the turnaround showed figures cut off at the knees, in a forest.** The turnaround prompt is now short and puts the framing first (three full-body views, head to shoes, plain white studio). Measured with the automatic image check: full bodies went from 0 of 5 renders to 10 of 10, and 12 of 12 on three different characters. - **No more wounds or odd faces from one-off moments in a character's description.** Anger ("rote Stirn bei Wut"), tears, trembling, twitching or a tongue over the lips were drawn as permanent features (red marks, a tongue sticking out); they are left out now, like wounds. Scars are left out of images entirely — even a faded scar described as fully healed came out as a fresh gash. - Removing a wound from a sentence no longer leaves half-sentences behind; a faded coat is not mistaken for a faded scar. ## [1.39.0] — 2026-09-21 ### Added - **Magazine review: select many boxes and act once.** *Select boxes* lets you click boxes or drag a rectangle over them, then change all their types or delete them in one step. - **Undo instead of "are you sure?".** Deleting, changing a type, editing text, merging and splitting happen at once and offer **Undo** (the latest change, while nothing newer has happened). - **Text inside a new picture folds into it.** When a region is drawn as, or changed into, a picture, the short text inside it (a chart's legend and axis labels) is no longer read aloud — no more deleting a dozen labels one by one. ### Changed - Deleting a single region no longer asks for confirmation; it can be undone instead. ### Fixed - Opening a region's text editor right after another change could have it wiped by the redraw still in flight. ## [1.38.1] — 2026-09-21 ### Fixed - **Magazine: "Could not approve: Unknown block ids".** After deleting a region, merging it, or changing a text region into an image, the review page still had the old id selected and Approve sent it; worse, it sent a stale image list that dropped a region just turned into an image. The review selection now follows every such change (a converted region keeps its selected state under its new id), Approve only sends ids that still exist, and deleting a region also removes it from every article that listed it. - Keyboard focus in the review list now lands on the row that is focused when the list is redrawn, not the one focused when a change started. ## [1.38.0] — 2026-09-21 ### Added - **Magazine review: quick corrections per region.** Edit a region's text in place and save it, split a region in two at the cursor, read one region again with OCR, or merge it with the next region (hyphenated words at the join are closed up) — each from its row, without leaving the page. The list can also be worked from the keyboard: ↑/↓ move, Space tick, Enter edit, R read again, M merge, Delete delete; the focused row keeps focus after a change. ## [1.37.0] — 2026-09-21 ### Added - **Magazine: "Clean up boxes"** — across a whole issue, short text inside pictures, one- or two-character OCR noise and boxes that only repeat a bigger box's text stop being read aloud, and author lines that run into the first sentence are split into the name and a paragraph of their own. Nothing is deleted (every change is a type you can undo in the review list) and the previous layout is kept as a backup. Runs automatically on import and *OCR again*. On a real 204-page issue: 71 texts in pictures, 25 noise boxes, 38 duplicates, 5 author lines. ### Fixed - Recomputing a page's reading order no longer drops box fields the layout model does not know about. ## [1.36.0] — 2026-09-21 ### Added - **Magazine review: set the reading order yourself.** Type a position number into any row to move that region, or press *Order by clicking* and click the boxes on the page in reading order (with *Undo last*; skipped boxes follow in their current order). A hand-ordered page is marked as such and kept: *Fix reading order*, type changes and newly drawn boxes no longer reshuffle it, until *Automatic order* hands it back. ## [1.35.0] — 2026-09-21 ### Added - **Magazine review: change a region's type or delete it from the list.** Every row on the review page has a type picker (paragraph, headline, subheading, standfirst, author, caption, pull quote, sidenote, page number, source/footer, advertisement, image) and its own delete button with a confirmation. Turning text into an image cuts the box out as a figure; turning an image into text reads it with OCR; the region stays in the article being reviewed. - **Magazine: "Fix reading order"** re-reads every page of an already imported issue column by column, without OCR, keeping text, boxes and approvals (and any page ordered by hand). - **Character images are checked and regenerated automatically.** Each profile picture and character sheet is inspected after it is rendered; a portrait or sheet that fails (wrong gender, three front views, cut-off feet, elf ears on a human, wounds, scenery behind a studio shot, a different face from the profile picture) is rendered again with a new seed, up to three times, and the attempt with the fewest faults is kept. The character page shows the outcome: checked with no faults, faults still found (listed), or not checked because the checker was unavailable — in which case it stops retrying rather than re-rendering blindly. - **The character sheet now shows the profile picture's face.** The turnaround and the story moment take their face from the profile portrait (face transfer after rendering), so the portrait and the sheet show the same person; in the story moment only the main character's face is replaced, not the extras'. - **Automatic check for generated character images** (`POST /api/character-image-check`). A vision model reports what it actually sees in a portrait, turnaround or scene — figure count, which way each figure faces, whether heads and feet are in frame, the apparent gender, pointed or round ears, wounds, plain or scenery background, and whether two images show the same person — and the app compares that with the character sheet. An unavailable checker or an unreadable answer counts as a failure, never a pass. It uses the LLM that is already loaded (no model swapping); calibrated at 14/14 on panels with known defects. - **LiteLLM model-group aliases in the model list.** The Active Language Model / OCR model dropdown now also lists the gateway's router aliases (ocr, default, cheap, vision, local-stt …), which LiteLLM's `/models` omits; they are read from `/router/settings` and shown first. Gateways without that endpoint are unaffected. ### Changed - **Searchable model dropdown** for the Active Language Model and Active OCR Model pickers (Engines page): the long native model list is now the same type-to-search dropdown used for voices, with model-specific wording and no avatars. - **Character images: period-safe, uninjured, less uniform.** Costume and appearance text sent to the image model is now rewritten to period garments (Uniform, Weste, Hut, Sporen, Jacke, Schal … became livery tabard, jerkin, felt cap, doublet …) because the extracted sheets contained such words for ~45 characters and the images came out in wartime/cowboy dress; injury clauses (blood, wounds, bandages, burns, limping, torn clothes) are removed clause by clause from appearance, costume and the story-moment text instead of dropping whole sentences; characters with armour or weapons get an individual garment cut and colour scheme (stable per name, colours named in the sheet win) instead of one shared list of gambeson/brigandine/plate. Saved prompts are cleaned the same way at render time. Adult figures are described as eight head-heights tall with a small head, to counter the short, stubby turnaround figures. The stored sheets are unchanged. - **Book setting reworded to Dark Eye high-fantasy medieval** (was "early Renaissance 1420-1500", which produced musketeer/wartime looks), soot/dirt removed like injuries, and prop tiles are object-only (they used to show a person). Existing image pairs are unchanged until regenerated. ### Fixed - **Magazine pages with two columns were read interleaved** (half a sentence from each column in turn). Columns are now found from where column-width text starts rather than from the whitespace between them, which loose OCR boxes and a pull-quote across the gutter had defeated; headlines and standfirsts are read before the columns under them, a caption beside a photo above the columns before them, and short pull-quotes at the end of the page instead of mid-sentence. Measured over a 204-page issue: 89 sentence continuations kept intact versus 85 before. Existing issues need *Fix reading order* once. - **Confirmation dialogs are announced to screen readers** (role alertdialog with title and message), app-wide. - The magazine review list's type column no longer overlaps the text on narrow screens, and the type and delete controls stay available on phones. - **Non-elves no longer get pointed ears.** The species test read the raw sheet field, so a hedged value such as "Mensch (oder Halbelfe, je nach Kontext, hier nicht spezifiziert)" matched "Halbelfe" and gave a human character elf ears — and that whole sentence went into the prompt as her species. Speculative sentences are now dropped from the species before either use, with more hedge wording recognised ("nicht spezifiziert", "je nach Kontext", "not specified", "depending on"). - **The story-moment panel keeps the character's gender, age and species.** It restates them ("one adult woman named Karyla") instead of relying on the subject clause, after a female character rendered as a man among male extras. - **An established scar renders as healed** — an old, pale, closed line — rather than as a fresh red wound. Scars are permanent and deliberately survive the injury filter, but the model drew them bleeding. - **Weapons are held as weapons.** A sword was rendered shouldered and aimed forward like a firearm; period arms are now described as gripped low by the hilt, resting on the shoulder, hanging at the side, or sheathed. - **Reverted the per-view turnaround renders.** Drawing front/side/rear separately with the edit model produced three near-identical front views, mixed styles between panels, cut-off bodies and a wireframe pose guide rendered into the costume. The turnaround is generated as a single three-panel image again, as it was before. - **Character-sheet extraction returned nothing with a reasoning model**: the blocking `/api/character-sheets` call could spend its whole token budget thinking and answer with empty content. It now retries once with thinking switched off (and once more without the switch if the backend rejects it). --- ## [1.34.0] — 2026-09-19 ### Added - **Verify / recast for span-mode casts** (`audiobookVerifySpans`): the app's Recast unknown, 2nd Quality Run and auto-repair entry points now run span-natively for a span cast instead of the classic passes that re-emit text. **Verify is advisory**: one extra differently-windowed pass (~10 min per novel) that never changes a speaker — agreeing lines are marked `agreed`, disagreeing lines keep the first answer, are marked `disputed` and record the other pass's answer as `altSpeaker`; the confidence score reflects both. **Recast unknown** re-asks only the Unknown lines with reasoning and fuller context (`attribute_only` on `/api/attribute-spans`) and leaves any line the model cannot place Unknown. Segments now carry a `spanId`. ### Fixed - **Visual-pair review no longer flags the same character reusing its picture in a later book** (`asset_reused_across_characters` now fires only when *different names* share one image). On the real library this removed 6 of 14 false-alarm groups (Darrag, Himgi, Lysandra, Marcian, Nyrilla, Narrator); the remaining ones are aliases or mix-ups worth a look (e.g. Alrik/Ritter/Zerwas share one image). - Span cast and verify now send the model **warm-up request** first (as the classic engine does), so a cold-booting model server no longer times out the first passage into tag guessing. - The cast view's **2.4 Verify all characters** now reaches the advisory second pass for span casts (it previously passed only `repairNarration`, which would have run the Unknown-only recast); its tooltip describes what it does for a span cast. ### Changed - The custom attribution prompt (Settings) applies to the classic engine only; span mode uses fixed instructions (documented). - Auto-repair no longer runs after a span-mode cast: Unknown lines stay visible for review instead of being guessed at. - **Span-cast confidence is calibrated on agreement between the two passes** instead of the old heuristics: agreed 82 % (Likely), disputed 48 % (Doubtful), Unknown 5 % (Guess), not yet verified 76 % (Probably) — pooled over three novels (~54 000 spoken words, 2 485 lines with a second opinion). The old bands claimed 16 % for lines that were 62 % right and the tag/role-label signals did not discriminate in span casts. Classic casts keep the old model. Running Verify therefore shrinks the "below 80 %" review queue to the disputed and Unknown lines. ### Measured — and rejected - On the real pipeline (Book 1, in-app), letting a reasoning question restricted to the two candidates overrule disputed lines made them **worse** (66.5 % → 61.4 % on 3 203 words), and the second pass "resolving" Unknown lines was ~3 % right, so no adjudication or silent filling exists. Lines both passes agreed on were 82 % right versus ~67 % for disputed first answers — that agreement is the signal used. An earlier offline experiment had suggested the opposite; it used a biased baseline (see `architecture/audiobook-attribution.md`). - Reasoning mode for the first pass (25× slower, no gain) and smaller/larger windows (−4 / ±0 points) were also tested and not adopted. - Final in-app run on Book 1: the advisory verify took 526 s, changed no speaker, and separated the lines well — 757 agreed lines were 83.2 % right, 118 disputed lines 48.5 % right (12.5 % of the dialogue holding ~28 % of the errors), which is what makes the flags useful for review. --- ## [1.33.0] — 2026-09-19 ### Fixed - **A matched image pair for a character's base appearance was never fully saved.** `_charAutoGenerateConsistentImages` persisted only the avatar; the concept/reference image and the pair metadata (prompt version, fingerprint, digests, `identity_consistent`) were set on the in-memory object and lost on reload, and the generic visual save wrote a base-appearance concept image to the record root instead of `sheet`. All visual results now go through one save path (`_libSaveCharacterVisual`) that writes the active form or the base correctly, digests included. - **Avatar-only generation could use a stale LLM-written prompt** and left an existing pair looking consistent. It now uses the deterministic portrait prompt (a hand-edited prompt is honoured) and marks an existing pair for review, since a lone regeneration gives a new face (same for reference-only). - **Sheet extraction no longer asks the LLM for `image_prompt`** (server and browser prompts) — the root cause of avatar/concept drift. The deterministic prompt is generated right after extraction. - **Prompts written while an appearance form was active landed on the base sheet** (regenerate and manual save) while the page kept showing the form's stale value; appearance prompts now go to the active form, character-wide prompts to the base. Inline Physical/Clothing edits likewise change only the active form. - **Visual-pair review ignored forms**: a character with an active form always looked unfingerprinted. Each form with images is now checked against its own metadata and fingerprint and reported under the form name. - **Fingerprint parity**: server and browser hashed list-valued fields and non-BMP characters differently, so such characters always looked stale; the fingerprint now also covers the `visual_*` overrides that change the prompt (appended only when set, so existing pairs stay valid). A test runs the browser code in Node and compares. ### Added - **Appearance forms panel** in the character detail page: add (starts as a copy of the base look), rename, delete (confirmed), show, per-form stale flag, generate matched images for one form or every form (prompts rebuilt per form first, active appearance restored), max 8 (server-enforced). - `architecture/character-visual-identity.md`; tests in `tests/test_character_identity.py` and `tests/characters.spec.js` (the persistence tests fail on the previous code). --- ## [1.32.0] — 2026-09-19 ### Added - **Span-mode audiobook casting.** Structure now comes from the book's own quotation marks (`static/js/audiobook-spans.js`, deterministic, pure, unit-tested): narration and dialogue spans keep the exact source text, and the LLM is asked only *who speaks each dialogue span* via the new `POST /api/attribute-spans` (`core/attribution_spans.py`) — it can no longer fuse, drop or duplicate text, and answers with a few tokens per line instead of re-emitting the passage. `audiobookCast()` dispatches to it by default (`static/js/audiobook-cast-spans.js`); `localStorage['ab-span-mode']='0'` restores the classic engine, and a book with no recognisable quote style falls back to it automatically. Drafts now record which engine made them (`mode`) so a resume never mixes window indices. - Handles the extraction damage seen in real books: closing marks whose opener was lost, openers whose closer was lost, multi-paragraph speeches that are not re-opened per paragraph, and quoted terms inside sentences. - **`scripts/score_attribution.py`**: word-aligned, alias-tolerant scoring of a cast against a hand-corrected one (coverage, narration/dialogue agreement, speaker accuracy, confusions). Insensitive to how narration is chunked, unlike `scripts/compare_casts.py`'s exact-text matching that dominated the old "fully correct" figure. - `architecture/audiobook-attribution.md` documents the design, contract, measurements and rollback. - `npm run test:unit` (Node's built-in runner) and `tests/test_attribution_spans.py`. ### Changed - The attribution prompt asks for the label the text uses when a person is only known by role ("Der Fremde", "Baronin") instead of forcing another known character's name; role → name resolution happens book-wide in the existing alias passes. Thinking is disabled for this lookup (≈25× faster on the same windows, no measurable accuracy difference). ### Measured (same scorer, same hand-corrected references) | | classic Run 3 | span mode | |---|---|---| | Book 1: narration/dialogue agreement | 93.6 % | 97.4 % | | Book 1: dialogue words right speaker (tolerant) | 60.6 % | 75.4 % | | Book 1: every word right (tolerant) | 87.0 % | 92.2 % | | Book 1: wall time | ≈ 4 h | ≈ 11 min | | Book 2 (not tuned on) every word right | – | 90.8 % | | Book 3 (not tuned on) every word right | – | 89.9 % | The references themselves contain label noise (inline narration folded into dialogue, inconsistent long monologues, some wrong speakers); differences of 1–2 points are not meaningful. --- ## [1.31.0] — 2026-09-16 ### Fixed - **"Edit boxes" (added in 1.30.0) silently did nothing — clicking it, or any of its move/resize/delete/save controls, was a dead click.** `magazine.js` wraps the whole module in an IIFE and only exposes functions the inline `onclick=` HTML attributes call by explicitly assigning them onto `root` at the bottom of the file; `magReviewToggleEdit`, `magReviewCancelEditPanel`, `magReviewSubmitEdit`, `magReviewDeleteRegion` and `magReviewClearCurrentPage` were never added to that list, so every one of those clicks threw an unhandled `ReferenceError` in the browser console with no visible feedback in the UI. Added the five missing `root.magReview* = ...` lines. - **"Edit boxes" could only resize a region from its bottom-right corner**, so two regions couldn't be split correctly along a shared boundary — e.g. a paragraph OCR'd as one box that actually belongs to two, where the fix is dragging *just* the line between them. Replaced the single corner handle with one handle per edge and per corner (`static/js/magazine.js`, `static/style.css`): dragging a side handle (top/bottom/left/right) now moves only that edge, leaving the opposite edge and both other dimensions untouched, so a shared boundary can be corrected on each box independently and then re-OCR'd. - The "Previous page"/"Next page" (article-scoped) buttons in the page navigator called `magReviewStepPage`, a function that did not exist anywhere in the file (also never exported) — they were also permanently disabled by a `state.reviewPageIdx` that was read but never set anywhere. Implemented `magReviewStepPage`, exported it, and derive the page index from `state.article.pages` at render time instead of the dead field. ### Added - **Prepare audio in the Magazine reader.** A new button synthesises the whole article ahead of time (two requests in flight, current position first, cancellable, with "Preparing audio n / N" progress) so Listen then plays straight through without waiting sentence by sentence. Even without it, playback now stays three sentences ahead of the current one. Synthesised sentences are cached in memory keyed by voice + text (`magSynth`), so a changed narrator or edited text can never replay stale audio, a failed request is never cached, and switching article discards the cache. Not persisted: a reload loses it. - **Searchable voice picker for the Narrator and podcast Host A/B/C selects** in the Magazine reader, replacing the plain dropdown with the app-wide picker (`voice-picker.js`). The `<select>`s were moved out of their wrapping `<label>`s: a label forwards clicks to its control, and the picker read that forwarded click as an outside click and closed itself instantly. The picker triggers get `role="button"` and an `aria-label`. - **Two-page spread view in Magazine review.** A new "Two-page spread" toggle next to the page navigator shows the current page next to its facing page (odd/even print pairing, e.g. 22/23), each with its own OCR-results list directly beneath it — a 2×2 layout matching how an open magazine spread reads. Only one page is draggable/drawable at a time; "Edit this page" on the other half switches which one the toolbar's draw/edit/OCR-again actions apply to, without leaving spread mode. Checkboxes and click-to-highlight work on both pages regardless of which is active. ### Verified - `node -c static/js/magazine.js` after each edit, plus a Python check that every `mag*` function named in an `onclick=`/`onchange=`/`onmouseenter=`/`onmouseleave=` attribute has a matching `root.mag* = ...` export (was non-empty before this fix, empty after). - `tests/magazine.spec.js`: picker replaces the select, search filters (incl. the "No voices found" state) and selects; Prepare audio synthesises each of a 4-sentence article exactly once before playback and Listen then makes no further synthesis requests; Prepare audio with no narrator is refused with zero requests. 9/9 on the desktop project (the iPhone project cannot launch in this environment — it pairs a webkit profile with the snap Chromium binary — and fails before any test runs, unrelated to this change). - Live via Playwright against the real ingested c't KI Wissen 2026 magazine, page 21 (the exact page/article from the bug report): clicked the real "Edit boxes" button (not a direct function call) and confirmed it now activates with no console errors; dragged region 7's bottom ("s") resize handle down ~40px with real mouse events and confirmed only its height changed (top/left/width untouched) and the Save/Save & re-OCR panel appeared; clicked Cancel so no test edit was written to the real article. Toggled "Two-page spread" and confirmed the 2×2 layout (page 20 + page 21 images on top, each page's own OCR-results list directly beneath it, "Edit this page" swapping the active/draggable page). --- ## [1.30.0] — 2026-09-15 ### Added - **Edit boxes in Magazine review.** In article review, a new "Edit boxes" mode lets you drag an already-detected region to move it, drag its corner handle to resize it, or press its × to delete it — for correcting a box the layout engine got wrong, not for one it missed entirely ("Draw it" still covers that). Moving or resizing opens a small confirm panel: "Save" keeps the region at its new position, and for text regions "Save & re-OCR" also reads the corrected area again and replaces its text. Deleting a region removes it from the page entirely. - New `core/magazine.py` `update_region()`/`delete_region()` and `PATCH`/`DELETE /api/magazine/{doc}/pages/{page}/regions/{block_id}` routes. An image region is re-cropped from the page render whenever its bbox changes, since its saved file is exactly that crop. ### Verified - Live against the real ingested c't KI Wissen 2026 magazine: added a throwaway region, moved it (bbox persisted, text unchanged), moved it again with re-OCR (correctly preserved the old text when the corrected area had no readable text — the same known engine limitation from 1.29.0, not a new bug), deleted it (confirmed gone; repeat delete correctly 404s). --- ## [1.29.1] — 2026-09-14 ### Fixed - **All Docker container status checks, start, stop and restart were silently broken** (`core/docker_client.py`): `_docker_get_json`/`_docker_post` called `sock.connect(sock_path)` a second time after `_docker_connection()` had already connected the socket, referencing a `sock_path` that was never defined anywhere in either function — every call raised `NameError`, surfaced to the UI as every engine card showing "Not installed"/"Not Found" regardless of what was actually running (found via the OCR engine cards and the Active OCR Engine model dropdown both showing this). Removed the redundant second connect call. Added `tests/test_docker_client.py` so this can't regress silently again. ### Docs - Recorded a request to make the magazine ingestion "watching it work" panel more visually engaging — draw detected region boxes live over the page image as each is classified, instead of the current static page-image-plus-text-list (TODO.md). --- ## [1.29.0] — 2026-09-14 ### Added - **Magazine figure descriptions.** In article review, "Describe" under a figure/chart/table/code block sends it to the configured vision engine (with fallback) for a one- or two-sentence description in plain language, type-aware (a chart gets a "what it measures and its trend" prompt, a table gets "headings and what stands out," etc.). Editable before approval. A new "Read figure descriptions" toggle in the reading controls speaks generated descriptions where they occur in the article; purely additive — off by default, and a figure with no generated description behaves exactly as before. - New `core/magazine.py` functions `describe_visual_region()`/`describe_visual_region_chain()` (same reliability guards as page OCR: repetition-loop detection, sentence-boundary truncation), `POST /api/magazine/{doc}/images/{id}/describe` (background job) and `PATCH /api/magazine/{doc}/images/{id}` (manual edit) routes, `descriptions=1` query param on the article-text endpoint. ### Verified - Live end-to-end against the ingested c't KI Wissen 2026 magazine (a real chart, article-008): ingest → Describe → persisted in layout.json → appears as a `figure_description` block in article text only when requested. - Found and documented a real config-dependent limitation, not a code defect: the currently-configured primary vision engine (baidu/Unlimited-OCR) is transcription-only — it returns empty content for any descriptive prompt regardless of phrasing (confirmed with three different prompts) — so description generation currently depends on a caption-capable fallback engine being configured and running. See Manual.md's Errors table and PROJECT_MAP.md's Known limitations. --- ## [1.28.2] — 2026-09-14 ### Fixed - **LiteLLM Gateway engine card connected using a stale global LLM key instead of its own saved key, after LiteLLM's key was rotated on a different port.** `/api/probe-url` only ever knew a card's *role* (`llm`), not which specific card it was, so it had no way to look up that card's own saved key — it fell back to the old global `llm_api_key`, got a 401 from `/v1/models`, then fell through `/api/tags` and `/api/version` (both genuinely 404 for LiteLLM) and reported the last of those as the failure, masking the real cause. Connect now passes the card's name through, and the backend resolves that card's own key from `engine_api_keys` before falling back to the global key. "Use as LLM" now goes through a dedicated endpoint that saves the same key Connect just verified (not a separately-typed, easily-stale value) alongside the normalized `.../v1` URL. ### Docs - Documented per-user provider-API-key storage as an explicit open item under the deferred multi-user/identity planning pass (`architecture/multi-tenant-and-identity.md`, `TODO.md`) — today's single global, gitignored, 0600 `settings.json` is adequate for one operator but has no per-user secret separation. --- ## [1.28.1] — 2026-09-14 ### Added - Recorded the user's explicit priority order for upcoming feature work in TODO.md: Magazine reader roadmap, then Character system, then Audiobook speaker attribution. No code change. --- ## [1.28.0] — 2026-09-14 ### Added - **Second deployment: `correspondence-nuc` (192.168.178.19), live alongside the DGX Spark instance.** No TTS/OCR engines of its own — every generation and OCR call crosses the LAN to the DGX, which required pinning `vision_ocr_url`/`vision_ocr_fallback_url` explicitly (their default, `host.docker.internal`, resolves to whichever host the container happens to run on, which is silently wrong off the DGX). Voice library is NFS-mounted read-write from the DGX's own directory (`/etc/exports` on the DGX, persisted in `/etc/fstab` on the NUC) — confirmed the NUC instance sees the exact same 344 voices, never a separate copy that could drift. - Deployed by building directly on the NUC from an rsynced copy of the working tree, rather than pulling the published `ghcr.io` image — that registry copy turned out to be both several versions stale and private with no credentials configured on the NUC. - New `docker-compose.nuc.yml`, tracked in this repo as the reviewable reference for that host (the existing `docker-compose.yml` stays the DGX's own — different build/network/socket-proxy shape). - Accepted limitation carried over from planning: a voice change still needs a manual restart of the DGX's TTS containers — no `docker-socket-proxy` on the NUC instance, since cross-host container control was never going to work through it anyway. --- ## [1.27.5] — 2026-09-13 ### Fixed - **App became unusable on mobile after a script failed to load, with no indication why.** The module loader fetched each JS file once with no retry; a single dropped request on a flaky mobile connection left that module's functions/variables undefined while every *other* module kept executing anyway — confirmed live as a voice-library crash ("fmtDbfs is not defined") and a save button reporting "No audio ready" despite a file being loaded and trimmed correctly, both traceable to the same root cause. `_load()` now retries up to 3 times with backoff before giving up, and if a module still can't load after that, the app now says so plainly ("Some parts of the app failed to load. Please reload the page.") instead of silently continuing in a partially-broken state. - **Eight real phone-width layout bugs, found and fixed by loading every section of the app at a 375px viewport and checking for content running off-screen:** - `.section-head`'s own mobile wrap rule targeted `.section-header`, a class nothing in the app actually uses — the icon/title/action row (e.g. "+ New voice") never wrapped and ran off-screen with no way to reach it, on every section. - The voice library's "Preview text" / "Reference transcript" toggle, and the Clone-a-Voice source tabs' mobile icon-only rule (`.clone-src-tab span:not(.mdi)`), matched a `<span>` wrapper the markup didn't actually have — added it, plus `aria-label`s so hiding the text doesn't erase the buttons' accessible names. - The Voice Design sample cards' real mobile override was silently dead: `voice-design.css` loads *after* `style.css` and re-declares the same selector unscoped, so it always won regardless of viewport. Moved the override into the file that actually controls the cascade. - Three separate `repeat(auto-fill, minmax(380-440px, 1fr))` engine-card grids (`.llm-service-grid`, `.llm-local-grid`, `.dc-grid`) couldn't fit even one column under ~460px; collapsed to one column with `min-width: 0` on their items (a grid item's default `min-width: auto` still won't shrink below its own content, which was the remaining floor after the column fix alone). - The routing test row (`minmax(120px)/minmax(110px)/minmax(220px)/auto`, 450px+ minimum) and the conversation input bar (text field, mic, noise-gate row and three toggles as one non-wrapping flex row) both collapse/wrap under 767px now. - Try It Out's EQ sliders shrank to fit next to their checkbox toggle instead of ever wrapping below it, so their `white-space: nowrap` labels overflowed their own squeezed container; forced them onto their own full-width line. --- ## [1.27.4] — 2026-09-13 ### Fixed - **Vision OCR decoding loops are now caught and treated as a failed call, not baked in as page text.** - A local vision model can degenerate into repeating the same short phrase hundreds of times instead of transcribing the page — confirmed live: a Qwen VL engine transcribing a population chart returned "das Produkt, das Produkt, ..." until it filled its whole response budget. That text was previously accepted as a real region and would have been narrated verbatim. - Added a general detector (any short phrase containing a real word, repeated 8+ times immediately back-to-back) alongside the two existing narrow signatures for specific LaTeX/equation-echo failures — the two existing checks only ever matched one engine's specific failure shape, this one is engine- and language-agnostic. Deliberately excludes patterns with no letters (a dot-leader contents line, a repeated number sequence), which are legitimate. - Applied at both call sites: the labelled-region path (`vision_ocr_page`) and the plain-text fallback path (`_whole_page_region`). A detected loop is treated as a failed engine call, so the existing fallback chain (secondary vision engine, then tesseract) gets a real chance instead. --- ## [1.27.3] — 2026-09-12 ### Fixed - **Figure-boundary snapping: sparse charts recovered, adjacent figures no longer unstable.** - A vision OCR engine's box often only lightly grazes a bar chart's thin bars, gridlines and axis labels — none of which reach the "solid ink" density a photo or UI panel does. A second, looser pass now grows the box through any real mark at all once a dense anchor (or the seed itself) is found, recovering charts that were previously left truncated at roughly a third of their real height. - Replaced iterative "re-join whichever component the seed happens to touch" merging with connectivity decided up front (dilate, then label once): the previous approach was seed-size-dependent, so re-running the pass on an already-corrected (larger) box could pick a *different* anchor and shrink a good result back down. Confirmed and fixed on the same chart that exposed the original truncation. - A merged bounding box can enclose a real paragraph sitting between two components even though neither component's own cells touched it; edges are now trimmed back to the nearest text wall found searching outward from the box's centre. - Two nearby figures fencing each other (so one's growth can't bridge into the other's territory) made each one's result depend on the other's *current* box — a feedback loop that, for two dark photos sharing a caption strip, never settled and swung by over 100pt between runs. The pass now re-seeds every visual on a page from every other's last result for a bounded number of rounds, and a result is only accepted once it reproduces itself when fed back through the same rules — so it is idempotent by construction, not merely by observation, including for the one confirmed pair that cannot mathematically converge. - Lowered the minimum seed/graphic overlap needed to reposition a badly-offset box, which was narrowly rejecting some genuine repositioning cases and forcing them to need a second "Fix figure boundaries" run before correcting at all. --- ## [1.27.2] — 2026-09-12 ### Fixed - **Live ingest preview finished and hardened** (was left as in-progress, uncommitted work from a previous session). - Formatting logic factored out into a standalone, unit-tested `_render_ingest_preview_html()` instead of living inline inside the progress callback. - Fixed a duplication bug: a figure's own transcribed text (usually its on-screen UI copy, e.g. "Post your new Instagram photos...") was rendered as an ordinary paragraph *in addition to* the new figure placeholder box. Figure/chart/table/code blocks are now represented only by the placeholder. - Furniture, sidebar, pull-quote and advertisement text is shown muted (previously dropped the `[LABEL]` bracket format entirely with no replacement) and is never promoted to a heading regardless of font size. - Replaced dead `getattr(bbox, "y0", ...)` calls (bbox is always a plain `[x, y, w, h]` list) with direct indexing, moved `import statistics`/`logging` to module level, and switched manual `&`/`<`/`>` replacement to `html.escape()`. - Replaced inline `style="..."` attributes with CSS classes (`.mag-ingest-preview-figure`, `.mag-ingest-preview-excluded`, `.mag-ingest-preview-more`) in `style.css`, matching the panel's existing dark console treatment. - Bounded the preview by content budget (accumulated per line, capped with an ellipsis marker) instead of a blind character slice that could truncate mid-tag. - The always-HTML output let the frontend drop its `startsWith('<')` content-type sniffing in favor of always using `innerHTML` (every text node is escaped server-side). - Marked the preview panel `aria-hidden` — it duplicates information already in the adjacent progress bar and would otherwise churn noisily for screen reader users. --- ## [1.27.1] — 2026-09-12 ### Added - **Roadmap: magazine contents view redesign and legal magazine-source browsing**, both scoped in `TODO.md` — no code change. - Contents view (`#s-magazine`'s article list): logged three candidate layouts as open options rather than picking one — a three-column page-thumbnail/text/figures view, a synthesis-state-and-inline-play list (the recommended quick win regardless of layout), and a flipbook with extracted text underneath. Needs a decision before implementation. - Magazine-source browsing: the two sites suggested as examples turned out to be unlicensed redistributors of current commercial titles, so no scraper was built against them. Scoped instead against sources that are actually usable — OPDS catalogues (also covers a self-hosted Calibre-Web/Kavita/Komga library), archive.org's public-domain periodicals, publisher-run free issues, and open-access journals. --- ## [1.27.0] — 2026-09-12 ### Fixed - **Figure boundaries are now derived from the scanned page, not from the OCR engine's guess.** - A vision OCR engine locates a photo far less precisely than it reads type: over all 138 figure regions of a 204-page issue, figure boxes were routinely offset, undersized, or sitting on the neighbouring text column while the transcribed text of the same regions was correct. - New `snap_visual_bboxes()` pass reduces the page render to an ink-coverage grid, seeds a connected-component search with the engine's box, and re-joins tiles a gutter apart — so a grid of cards or a chart beside its key comes back as one figure. Text blocks are excluded from the mask, which stops a figure swallowing the column beside it. - Conservative by design: a candidate must be solidly inked *or* framed on nearly all of its rows and columns (which is what admits a white-interior UI screenshot), may not shrink below 30% of the engine's box, and reverts when two figures are pulled onto the same graphic. Anything it cannot place confidently keeps the box it had. - Runs before the figure crop in both OCR paths, so the images shown during playback are framed correctly too. Corrected 78 of 138 figure boxes on the reference issue. - Replaces `refine_picture_bounds()`, which only recognised coloured panels and had no text fence. ### Added - **"Fix figure boundaries" in magazine review**, and `POST /api/magazine/{id}/figures/refine` behind it. - Re-snaps every figure box in an issue as a restart-safe background job. No OCR engine is called, so an issue ingested before this release is corrected without paying for its transcription again. - Text, article grouping and existing approvals are untouched — safe to run halfway through a review. Reports how many boundaries moved. --- ## [1.26.14] — 2026-09-10 ### Added - **Studio-Style Side-by-Side Magazine Upload Layout.** - Re-designed the Magazine upload card to mirror Studio mode's side-by-side layout: left panel contains optional Title & Issue metadata inputs plus a prominent **Durchsuchen** button (`<span class="mdi mdi-folder-open-outline"></span> Durchsuchen`), and right panel contains the clean `.reader-dropzone`. - Removed the redundant `+ Add magazine` button for a clean, direct, one-action upload flow. --- ## [1.26.13] — 2026-09-10 ### Fixed - **App-Wide Drag-and-Drop Safety Net & Dropzone Reliability.** - Installed an eager app-wide capture-phase `dragover` / `drop` safety net in `utils.js` that intercepts file drops on `window` and `document` to guarantee the browser never navigates away to open a dropped PDF in a new tab. - Improved `#mag-dropzone` drag event handling with nested element drag-depth tracking (`dragCounter`), preventing dragover class flickering when hovering over dropzone titles, icons, or buttons. - Direct file passing in `magCreate(file)`, removing programmatic `<input>` DataTransfer mutation to eliminate double invocation and race conditions during drag-and-drop. - Cleaned up duplicate variable scope declarations in `renderReview()` in `magazine.js`. --- ## [1.26.12] — 2026-09-10 ### Fixed - **Page-Wide PDF Drag-and-Drop Interception & Dropzone Navigation Hook.** - Installed an immediate, eager `window` drag-and-drop safety net (`installGlobalMagDropGuard()`) that prevents dropping a PDF anywhere on the page from opening the PDF in the browser tab. - Added `window.magOnShow()` navigation hook in `nav.js` and guaranteed `setupMagDropzone()` wires the `#mag-dropzone` target whenever the Magazine section is visited or mounted, stopping `magInit()` from exiting silently if the DOM element was not yet present at script load time. --- ## [1.26.11] — 2026-09-10 ### Added - **Comprehensive OCR Element & Grounding Recognition.** Full support for layout-aware vision OCR models (Baidu Unlimited-OCR, DeepSeek-OCR, DeepSeek-OCR-2, PaddleOCR). - Explicit element parsing for Headlines, Paragraphs, Sidenotes/Sidebars, Page Numbers/Folios, Images/Figures, Graphics/Charts/Maps/Treemaps/Diagrams, Tables, and Code blocks. - Native support for grounding coordinate tokens (`<|ref|>...<|/ref|>`, `<|det|>...<|/det|>`, single/double brackets, normalized `0..1000`/`0..1` scale, and pixel space). - **Ingestion Preview & Visual Thumbnail Cards.** The live ingestion preview formats non-text visual regions (`[CHART]`, `[MAP]`, `[TABLE]`, `[SIDENOTE]`, `[CODE]`) with clean badges instead of blank gaps, and the magazine review sidebar renders interactive cropped image thumbnails (`mag-block-thumb`) for detected visual figures. --- ## [1.26.10] — 2026-09-10 ### Fixed - **Instant Dropzone Upload & Active Progress Card.** Dropping a PDF (or picking a file) now automatically initiates upload and ingestion immediately. The dropzone transforms into an active progress card with a spinning loader, animated progress bar, status note (`Uploading "filename.pdf" (X MB)`), and disabled input controls so users get un-missable, immediate feedback. --- ## [1.26.9] — 2026-09-10 ### Fixed - **PyMuPDF Ingestion Requirement.** Provided actionable installation guidance when `pymupdf` is missing in runtime. - **Drag & Drop Upload Target & Feedback.** Made the magazine upload dropzone fully clickable, added file selection feedback showing filename and file size, and added active loading states with spinners and disabled controls during upload. - **Optional Title & Auto-Fill.** Relabeled Title input to optional and auto-fills title from document filename if left blank. --- ## [1.26.8] — 2026-09-09 ### Added - **Automatic Vision Layout Interpretation.** Magazine ingestion can now use a dedicated vision-language model after Unlimited-OCR. It receives the original page and all positioned OCR candidates together to identify article headlines, bylines, columns, paragraphs, sidebars, sources/footers, folios, ads, images, charts, statistics, tables, captions and code before article/chapter creation. - **Explicit Review Classifications.** The exceptional manual-review workflow can add a headline, page number, source/footer, or advertisement directly. Non-narratable types stay out of audiobook text by design. ### Changed - **Magazine Parsing Preserves Whole-Page Context.** The semantic pass now keeps up to 120 regions from a page in one request, rather than splitting normal complex pages into contextless groups of 60. - **Richer Layout Labels.** Sources, citations, footnotes, infographics, statistics and adverts are mapped to their semantic types instead of falling back to generic article body text. --- ## [1.26.7] — 2026-09-09 ### Fixed - **Released Magazine UI Updates Now Reliably Reach Browsers.** The HTML bootstrap version was stale at `1.26.1`, causing the browser to reuse an immutable old `magazine.js` bundle despite newer server deployments. All bootstrapped assets now use the current release version so current magazine review controls, whole-issue navigation and OCR tools load. ### Changed - **Magazine Deletion Is Discoverable.** Every magazine card now has a visible, labelled **Delete** button below it; confirmation remains required before the source PDF, OCR data, articles and generated assets are permanently removed. --- ## [1.26.6] — 2026-09-09 ### Added - **Safer Magazine Deletion.** Each magazine card now offers a confirmed delete action that removes its source PDF, OCR/layout data, articles and generated assets together. - **Rectangle-to-Metadata Extraction.** In article review, **Pick from page** beside Title and Author OCRs a drawn rectangle directly into that metadata field without adding it to narratable article content. - **Whole-Issue Page Navigation.** Review now has a separate **Magazine p. N / total** navigator, allowing every PDF page to be inspected across article boundaries while keeping the article-only navigator available. ### Changed - **Review Selection Controls Are Clearer.** **Draw image** is now **Draw graphic**, and **Clear all fields** resets all selected text and graphics on the current page for clean manual article-boundary review. - **Headline Detection Is Not Pretended to Be a Percentage.** The fixed 0.55 fallback now displays as **headline detected** rather than a misleading repeated `55%` confidence badge. --- ## [1.26.5] — 2026-09-09 ### Fixed - **“All magazines” Returned to the Wrong Place.** The contents-page button now clears the remembered active issue, refreshes the actual magazine library, and reliably returns to it instead of only switching an internal view. ### Added - **Magazine First-Page Thumbnails.** Each magazine card now displays page 1 of its source PDF as a lazy-loaded, aspect-ratio-preserving thumbnail. --- ## [1.26.4] — 2026-09-09 ### Added - **Native Baidu Unlimited-OCR Layout Parsing.** Magazine ingestion now uses Unlimited-OCR's documented `document parsing.` mode and consumes its `<|det|>` bounding-box regions. Images, non-text graphics, charts and tables are retained as visual regions instead of being mixed into narratable article prose; captions remain captions. ### Changed - **Magazine OCR Preview Uses Its Space.** The source page fills the available preview height while preserving its aspect ratio. A draggable divider lets readers allocate space between the page and recognised text, and remembers the choice. - **Character Sheets Read Like Profiles.** Profile fields are now one-column entries with a bold heading and normal prose beneath it, rather than compact label/value columns. The live passage/output drawer stays bottom-anchored and its top grip resizes from the expected edge. ### Fixed - **Unlimited-OCR Repetition Guard Was Not Activated.** Requests now include the model's required per-request n-gram settings and `gundam` image mode. Repeated fabricated LaTex and instruction-echo responses are rejected so fallback OCR can be used instead of saving poisoned page text. - **Character Line Totals Missed Aliases.** A profile now totals dialogue credited to its canonical name and recorded identity aliases (for example, `Larissa` and `Schwester Larissa`) and refreshes the total during casting. - **Page Furniture Does Not Become Article Text.** Native OCR regions now identify headers, footers and page numbers as page furniture, keeping them out of narration while preserving their position for review. --- ## [1.26.3] — 2026-09-09 ### Fixed - **Poisoned Vision-OCR Responses Became Article Text.** A malformed vision response that echoed its own formatting instruction and repeated literal `\\n equation` tokens was being accepted as a plausible full-page transcription. It is now rejected so the fallback OCR engine can run and invalid text is never saved to a magazine page. - **Character-Sheet Casting Could Stall and Ignore Cancel.** The configured llama-swap gateway now uses its reliable non-streaming completion path for character sheets instead of malformed pseudo-stream responses. Cancel now aborts the active browser request immediately and prevents further passages from being scheduled. ### Changed - **Visual Character Details Are Always Visible.** Character sheets now retain their *Erscheinung* section while a passage scan is still looking for visual evidence. It explicitly covers body/appearance (height, build, hair, eyes, skin and marks), clothing, and signature items, rather than silently hiding the section while empty. --- ## [1.26.2] — 2026-09-09 ### Fixed - **Character-Sheet Generation Could Stop on HTTP 405.** If an OpenAI-compatible LLM gateway rejects the configured version-prefixed chat route (`/v1/chat/completions`) with 405, character-sheet extraction now retries the gateway's root chat route (`/chat/completions`). This applies to both the live stream and its non-streaming fallback, so an otherwise healthy gateway no longer drops the current passage. --- ## [1.26.1] — 2026-09-08 ### Added - **Semantic Magazine Layout Recognition.** Magazine OCR review now distinguishes article headlines, internal headings, paragraphs, bylines, sidenotes, pull quotes, captions, images, charts, tables, code boxes, page numbers, running furniture, and advertisements. A constrained multimodal LLM pass groups fragmented OCR boxes by their visible column and paragraph layout, finds visual regions OCR missed, preserves tables and code formatting, marks text continued from the preceding page, and crops detected visual regions for article playback. Full-page adverts collapse into one excluded region, while adverts sharing a page with editorial content are kept separate. - **Direct Image Drawing in Magazine Review.** Review now offers separate **Draw text** and **Draw image** actions. A hand-drawn image or chart is linked to the current article automatically, with an option to exclude enclosed OCR text from narration. Detected images can also be included or excluded directly from the review list. ### Changed - **Clearer Magazine Review Navigation.** Page navigation is larger and more prominent, drawing controls sit beside it, existing OCR boxes fade during drawing, Escape cancels drawing, pointer coordinates stay within the page, and review opens on the article's first page instead of the preceding context page. - **Studio Character Sheets Fill the Browser.** The embedded character-sheet workspace now follows the full desktop flex height through the Studio phase and uses all remaining viewport space instead of stopping at an 880px/90vh ceiling. ### Fixed - **Bare OCR Server Addresses Failed With HTTP 404.** OpenAI-compatible OCR URLs without an explicit path now receive `/v1` automatically, so the configured Baidu Unlimited-OCR endpoint reaches `/v1/chat/completions`. Invalid responses and HTTP failures are logged, and failed retries preserve the existing page layout. - **Re-OCR Broke Reviewed Article Links.** When OCR replaces a page's region IDs, article block and image selections are remapped by page geometry, prior approvals return to review, newly detected visual regions are attached to the closest article, and page-wide scan images are no longer mistaken for article figures. --- ## [1.26.0] — 2026-09-07 ### Added - **Split-Screen Article Review, "OCR Again", Reading-Order Fixes.** Review shows the rendered page on the left with a numbered box per detected region and the extracted text on the right with matching numbers — clicking either side highlights the other. Building this immediately surfaced a real bug on live production data (see Fixed below). A per-page **"OCR again"** button forces that one page through the configured vision engine again without touching article grouping or approvals (`reocr_page`); a magazine-level **"OCR again (whole magazine)"** re-reads every page and re-runs detection, with a confirmation when articles were already reviewed since that resets their status. ### Fixed - **Two Real Magazine Columns Interleaved Into One.** Found live using the new split-screen review's numbered boxes on a real scanned c't magazine page: a photo caption at 49.9% of the page width sat in the gutter between two genuinely separate columns and merged them into a single detected column, so every block on the page came back `column=0` and reading order fell back to pure top-to-bottom — splicing the two columns' sentences together (a box the review numbered 6, physically above the box numbered 1, was read after it). Column boundary detection now excludes blocks much wider than the page's median block width; separately, the real gutter measured on that page was itself only ~2.1% of the page width, narrower than `detect_columns`' 3.5% threshold — lowered to 1.5%. - **Magazine "Forgets" the Open Document.** A full page reload or the browser's own Back button tears down all in-memory state; the magazine section always landed back on the empty library with no way back to what was open. Now persists the last-opened magazine id and reopens it automatically. The actual root cause of a reported duplicate: Studio/Reader's book library and Library → Books both listed every document with no `kind` filter, so the magazine was already showing up there disguised as a plain book — it had not vanished, it was misfiled, which is what led to a re-upload and a genuine duplicate under a different kind. Both listings now exclude `kind: "magazine"`. - **Confidence Badge Layout (Studio Characters Review).** The badge rendered on its own line far below the speaker name because both the name and the article text were `display: block`, forcing a line break regardless of markup order. Wrapped the name, badge and check button in a shared flex row so the badge now sits directly beside the name. - **docker-socket-proxy Crash Loop.** `read_only: true` left the container's own entrypoint nowhere to write its `haproxy.cfg`, crash-looping it and leaving the app without Docker access. `cap_drop: ALL`, `no-new-privileges` and the `:ro` socket mount already provide the real hardening, so `read_only` was removed for this one service. - **Magazine Dropzone Opened the Dropped File in the Browser Instead of Accepting It.** `dragover`'s `preventDefault()` was gated on `dataTransfer.types` including `'Files'`, which Chromium does not reliably populate synchronously on every `dragover` event during a real OS file drag — the condition failed often enough that the element never reliably became a valid drop target, so the browser fell through to its native "open the file" behaviour (and the hover highlight, gated the same way, never reliably appeared either). Removed the gate; `dragenter` is now also handled, not just `dragover`. Added a window-level `dragover`/`drop` safety net so a drop landing even slightly outside the zone can no longer trigger the browser's default action either. ### Added - **Live Ingest Preview, Region Type Icons, Article-Ownership Badges, Manual Region Drawing.** Modelled on the Baidu Unlimited-OCR demo UI: while a magazine is being ingested, a dark console-styled panel now streams the page image alongside its recognised text as each page finishes, instead of a bare progress bar (the raw text is written to a per-job `ingest_preview.json` file rather than the background-job `result` field, which strips text-like keys and caps content for data minimization). In split-screen review, every numbered box and its matching list entry now carries a centred glyph for its kind (title/deck/byline/body/caption/figure/…) and, when a region on a shared page belongs to a *different* article, a small badge naming that article — previously a region already claimed elsewhere looked identical to an unclaimed one. When the layout engine misses a region entirely, a **"Draw it"** toggle lets the user click-drag a box directly on the page image; on release it prompts for text-or-image and either uses typed text, OCRs the crop through the configured vision engine, or (image) crops and saves it as a new figure — added via `add_manual_region()` / `POST /api/magazine/{doc_id}/pages/{page}/regions`. - **Drag-and-Drop Dropzone For Adding a Magazine.** The "Add a magazine or paper" form used a plain `<input type="file">` where the Book/Source screen already has a proper drop target; now reuses the exact same `reader-dropzone` component (same CSS, no new styling needed beyond the surrounding grid) — drag a PDF onto the zone or click "Choose file", both routed through the same hidden input via a `DataTransfer` handoff so `magCreate()` needed no changes. Shows the chosen/dropped filename in place of the default hint text. - **Magazine P1 Complete: Images, Play-All, Word Highlighting, Driving Mode.** Figures now appear automatically during playback — `assign_images_to_articles` links each extracted image to the article whose column it falls in (the nearer article's blocks decide when two share a page), and the client shows "the last image reached so far" as playback advances past its approximate reading-order position. **Play all articles** (contents screen) queues every approved article in order and advances automatically at the end of each, rather than stopping and returning to contents. **Word-level highlighting** is available alongside the existing sentence highlighting — clearly documented as an *estimate*, not a measurement: no forced-alignment or TTS timestamp source exists in this pipeline, so each word's share of the sentence's real, measured audio duration is distributed proportional to its character length, a common karaoke approximation that can drift on heavily punctuated sentences. A checkbox turns it off; sentence highlighting (measured via `audio.onended`, not estimated) is unaffected. **Driving mode** is a full-screen overlay with two oversized controls and nothing else to interact with, reusing whatever is already playing/queued rather than its own playback path. **Sidebars and pull-out boxes are now excluded from narration automatically** (`mark_sidebars`): a column much narrower than the page's regular grid is reclassified `BLOCK_SIDEBAR` and dropped from an article's narrated blocks the same way adverts already are — review still shows every sidebar block, unticked by default, so nothing is silently discarded. Fixed a real defect found writing this: a full-width headline was left in the column-width analysis and could bridge two genuinely separate columns into one, hiding a real sidebar column entirely (caught by a failing test before the fix). 20 new tests. - **Second Vision OCR Engine, With a Real Fallback Ladder.** KDL Frontier Parser nano is now wired in alongside ocr-baidu-unlimited-ocr (`vision_ocr_page_chain`: first usable result from an ordered engine list wins, tesseract only if every vision engine fails). Confirmed live: KDL answers roughly 3x faster (~12s vs ~40s per page on the same test page) but returns **plain text with no coordinate markers at all**, unlike Baidu's labeled `text [x0,y0,x1,y1]content` regions — handled by treating its whole response as one ambiguous full-page block (`_whole_page_region`, gated by the same `ocr_quality_score` threshold), which loses column/reading-order structure for that page but is still a real improvement over tesseract. Settings gained `vision_ocr_fallback_url`/`vision_ocr_fallback_model`. - **OCR Engines Page Now Matches the LLM Engines Layout.** The OCR & Document Parsing page had no `.s-engines-page[data-page="ocr"]` markup at all — `renderLocalContainers` already supported an OCR grid generically (the same code handles TTS/STT/LLM/OCR identically), but `$('dc-grid-ocr')` was always null, so the card grid silently never rendered and the page showed only a header with "Add Custom"/"Refresh" buttons and nothing else. Added the missing panel: an **Active OCR Engine** picker mirroring **Active Language Model** exactly (same endpoint/model selects, same `/api/conversation/llm-models` fetch, which already proxies to whatever URL it's given rather than assuming the LLM setting) plus a **Local** grid, so both configured engines now appear as ready-made cards (Connect/Start/Restart/Use as OCR/Show code snippet) with no "Add Custom" dialog needed for either. - **Vision OCR Escalation For Magazine Ingestion.** A page that needs OCR, or whose already-extracted text scores as mostly garbage (`ocr_quality_score`) — native or tesseract alike — is now escalated to a connected vision OCR engine (`vision_ocr_url`/`vision_ocr_model` in Settings, defaulting to `ocr-baidu-unlimited-ocr` via `host.docker.internal`), with tesseract kept only as the automatic fallback if the call fails or the engine is unreachable. Verified live end to end on a real 28-page scanned c't magazine excerpt (Epson ES-580W, no scanner OCR): every previously garbled article title — `K | soll Arbeit abnehmen…`, `. |`, `@)X`, `Owns mee`, `Add | Postnew` — was replaced by the real headline (`1. Irrtum: KI ist ein allgemeiner Jobkiller`, `3. Irrtum: KI entlastet Fachkräfte`, `Wie KI Entwickler-Jobs verändert`), and furniture block detection jumped from 44 to 151 blocks because the engine labels running headers/footers/page numbers directly (`text [x0,y0,x1,y1]content`, `footer […]…`, `page_number […]…`) instead of needing after-the-fact fuzzy matching. 26 of 28 pages used the vision path; the remaining 2 fell back to tesseract automatically. `ocr_quality_score` is deliberately coarse — it catches text that is mostly symbol noise, not a plausible-looking single-word misread inside an otherwise normal sentence, which needs a dictionary or language-model check and is out of scope here. A vision-labeled block (`source: "vision_label"`) is trusted outright and skips `classify_blocks`' font-size heuristics entirely, since the engine gives no reliable font metric — an ambiguous `"text"` region still gets a font-size *estimate* from its paragraph area and length (`estimate_font_size_from_area`) and goes through normal classification. Off unless a URL and model are configured (`vision_ocr_enabled` setting, default on if reachable; per-request override via the ingest endpoint). 28 new tests. - **Magazine Projects (backend + API).** A second document workflow beside books, for magazines and papers where the audiobook assumptions break down: several unrelated articles share a page, columns run in parallel, adverts and sidebars interrupt body copy, and a feature routinely jumps twenty pages to its tail. Narrating such a PDF page-by-page walks out of an article mid-sentence and into an advert, so the canonical object is the **article**, not the page. `core/magazine.py` reconstructs articles from a flat PDF: PyMuPDF extracts text blocks with bounding boxes and font metrics, tesseract supplies a TSV fallback (with coordinates, so scanned pages join the same column grouping as native ones), blocks are classified as title/deck/byline/body/caption/pull quote/furniture/advert, ordered column-major, and grouped into articles anchored by the parsed table of contents, with `continued on page N` tails linked back to their originating article. `routes/magazine.py` adds 13 endpoints; ingestion is a restart-safe background job, since an 84-page issue takes minutes and the browser must not have to stay open. Magazines reuse the Read Aloud store (`kind: "magazine"`), so library listing, covers, progress and audio endpoints work unchanged. Verified end-to-end on a synthetic 8-page issue — the two-column feature was recovered as pages [3, 8] with its jump tail absorbed, the advert page excluded, and both bylines extracted — and on an image-only rendering of the same file, where all 47 blocks came back through OCR at 0.75-0.90 confidence. 44 tests. **The Studio UI is not built; this is reachable only via the API.** - **Podcast Mode For Articles.** An approved article can be rendered as a discussion between two or three voices the user picks, instead of straight narration. The configured LLM writes the script through the existing OpenAI-compatible client; the prompt forbids inventing facts, figures or quotes absent from the source, and tells the hosts to say so when the article reaches no conclusion. The parser tolerates a markdown fence and surrounding prose, and reassigns an unrecognised host label rather than dropping the turn — an unknown label would otherwise leave a line with no voice at all. The chosen cast is remembered so the next article defaults to the same hosts. Generation is refused for unapproved articles, since unreviewed boundaries may include a neighbouring article or an advert. - **Documentation Baseline.** `PROJECT_MAP.md`, `Manual.md` and `architecture/` are required by the project's own agent instructions but had never been created. All three now exist; the architecture SOP records the decisions that are easy to get wrong and the limitations that remain, including that OCR-engine selection is not yet dispatched server-side. - **Magazine Reader UI.** The Studio sidebar gains a Magazines section with the full P1 flow: a library of magazine projects, a contents tree grouped by section with per-article confidence, page range, listening estimate and audio status, the Stage D review screen (tick/untick layout blocks, correct title and author, approve or reject), a reflowed article view with click-to-seek sentence highlighting, a linked original-page view that shows the bounding box the current sentence came from, and a podcast panel (2-3 host voice pickers, script generation, turn-by-turn playback with highlighting). Verified with Playwright against the real backend contract: library listing, review pre-selection, approval, sentence highlighting and PDF-page linking, and the podcast gate on unapproved articles, plus a full navigation pass with zero console errors. Reuses the existing TTS preview endpoint for synthesis, so no new audio pipeline was needed. ### Changed - **Server-Side Layout Analysis For Magazines.** PDF text extraction for plain books stays in the browser, but magazine layout analysis runs server-side: PyMuPDF exposes real font metrics and embedded-image rectangles that PDF.js does not, OCR coordinates merge with those boxes in one coordinate space, and the result is expensive enough that it must be computed once and persisted rather than recomputed per browser session. PDF.js keeps rendering duty. Docling and Unstructured were rejected — multi-GB of torch and layout weights, ARM64 wheel risk on the DGX Spark and GPU contention with the TTS stack, to improve on heuristics that handle the observed cases. Adds `pymupdf` and `tesseract-ocr` (deu/eng) to the image. ### Fixed - **Audiobook Pipeline Nav Bar Bled Into Unrelated Pages.** `_wfUpdateHeaderNav` injected the audiobook workflow's "Previous / Next" bar and step hint into every `.section-head` in the document, with Studio as the only named exclusion. Confirmed live: the Engines page (LLM/STT/TTS/OCR/Image Generation all share one `.section-head`) showed a "Previous … Cast Audiobook ›" bar and a "STEP 1/8 Import a PDF…" hint that made no sense there, since the pipeline's three destinations are Read Aloud, Library and Script Rehearsal only. Changed from a blocklist (naming each unrelated section as it was noticed) to an allowlist, so the same bleed-through cannot recur on Magazines, Settings, or any future section. - **Captions On Photo Pages Would Have Been Read As Body Text.** Block classification judged font size against a baseline computed per page, so a page carrying a full-bleed photograph and a single caption made that caption its own reference: at a ratio of exactly 1.0 it classified as body copy and would have been narrated as part of the article. The baseline is now measured once across every block in the issue. - **Advert Pages Swallowed By The Preceding Feature.** Publishers print "ADVERTISEMENT" once to disclaim a page, not on every block, so the advert's own display type classified as a headline. Combined with an article claiming its whole page range, a full-page advert between two features was absorbed into the first and would have been highlighted as part of it in the PDF view. The label now carries its page, and an article claims only pages it actually contributes text to. - **Covers And Contents Pages Proposed As Articles.** The cover, the table of contents and full-page adverts each became a low-confidence article the reviewer had to reject by hand on every issue. A page whose only narratable content is a headline now starts no article, and the contents page — which lists articles rather than being one — is excluded. ### Changed - **OCR custom-card persistence.** Browser-local and server-backed custom OCR profiles are now merged during reloads, so a newly added Baidu or other OCR engine remains visible while settings synchronize; the OCR page also shows an actionable empty state when no profiles exist. - **OCR engine category.** Engines now includes local OCR/document-parser profiles for KDL Frontier Parser nano and Baidu Unlimited-OCR, with custom OCR profiles, connection probes, page-image API guidance, and future Magazine workflow integration documented in the TODO roadmap. - **Initial audiobook casts preserve dialogue boundaries.** The final cast normalization now merges adjacent narration only; same-speaker dialogue rows remain separate so Run 3 segmentation errors stay visible and do not become fused multi-turn lines. Recast/verify retains its explicit dialogue-only merge behavior. - **Dialogue-only merge regression coverage.** Recast/verify normalization now has explicit browser coverage proving that only adjacent known-speaker dialogue merges; narration and unknown-speaker lines remain separate. - **Visual identity regression coverage.** Portrait and concept-sheet prompt tests now verify shared identity anchors; completed pairs centralize prompt-version, identity-fingerprint, and asset-digest metadata. - **Frontend namespace migration completed.** The remaining utility, library, cast-compatibility, i18n, and settings/log surfaces now keep implementation state private while preserving explicit legacy compatibility exports. - **Conversation/log namespace migration.** Settings log state and About rendering helpers now use a private scope with compatibility exports and a `window.TTVC.conversation` API. - **i18n namespace migration.** Language dictionaries and mutable language state now use a private scope with compatibility exports and a `window.TTVC.i18n` API. - **Audiobook-cast namespace migration.** Cast serialization, page stamping, and speaker normalization now use a private scope with compatibility exports and the existing `window.TTVC.audiobook.cast` API. - **SillyTavern namespace migration.** Character-card parsing, import, and export helpers now use a private scope with compatibility exports and a `window.TTVC.sillyTavern` API. - **Seed-finder namespace migration.** Seed sample generation, caching, and batch controls now use a private scope with compatibility exports and a `window.TTVC.seedFinder` API. - **Character-column namespace migration.** Character-table metadata, persisted column visibility, and completeness helpers now use a private scope with compatibility exports and a `window.TTVC.characterColumns` API. - **Constrained character pagination.** Character-library pages now stop at 25 rendered characters on every device, preserving incremental loading while keeping the large-cast DOM below the measured 5,000-node budget. - **Character pagination budget.** Character-library pages render bounded batches, keeping large casts below the constrained DOM budget while retaining incremental “show more” loading. - **Utility namespace migration.** Shared DOM, status, accessibility, language, tab, and low-memory helpers now use a private scope with explicit compatibility exports and a `window.TTVC.utils` API. - **Voice-inspector namespace migration.** The three-pane voice inspector now keeps its selection state in a private scope while preserving `selectVoice` and exposing `window.TTVC.voiceInspector.select`. - **Character-library namespace migration.** Character rendering, detail views, avatar handling, and generation actions now live behind a private scope with compatibility exports and a `window.TTVC.characterLibrary` API. - **Character-sheet namespace migration.** Character-sheet extraction, prompt helpers, and Reader/Rehearser entry points now use a private scope with compatibility exports and a `window.TTVC.characterSheets` API. - **Voice-library namespace migration.** Voice loading, rendering, benchmarking, and add/import workflows now use a private scope with live compatibility accessors and a `window.TTVC.voiceLibrary` API. - **Generation namespace migration.** WAV merging, chunked TTS, history, playlist, effects, and refinement helpers now use a private scope with explicit compatibility exports and a `window.TTVC.generation` API. - **STT namespace migration.** STT backend selection, microphone recording, voice loading, and STT-to-TTS helpers now use a private scope with live compatibility accessors and a `window.TTVC.stt` API. - **AI-backend namespace migration.** LLM snippets and backend interaction controls now live behind a private section scope with an explicit `window.TTVC.aiBackends` API for cross-module snippet initialization. - **Integrations namespace migration.** Endpoint helpers, active-voice listings, virtual design IDs, and API snippet rendering now use a private scope with explicit compatibility exports and a `window.TTVC.integrations` API. - **Combined-library namespace migration.** Production books/plays, Rehearser and Reader handoffs, and cast integration now use a private scope with explicit compatibility exports and a `window.TTVC.library` API. - **Engine support namespace migration.** ElevenLabs browser state, custom engine cards, and local-container discovery now use a private scope with explicit compatibility exports and a `window.TTVC.engines` API. - **Routing namespace migration.** Route configuration, sound selection, speed previews, routing tests, and routing-log rendering now use a private scope with live compatibility accessors and a `window.TTVC.routing` API. - **Voice-source namespace migration.** Source URL management, filtering, source metadata normalization, and library import helpers now use a private scope with explicit compatibility exports and a `window.TTVC.voiceSources` API. - **Voice-clone namespace migration.** Clone audio loading/upload, microphone monitoring, design presets, and voice-design actions now use a private scope with explicit compatibility exports and a `window.TTVC.voiceClone` API. - **TTS preview namespace migration.** Backend voice selection, Fish emotion tags, preview synthesis, stream fallback, and performance-history helpers now use a private scope with explicit compatibility exports and a `window.TTVC.tts` API. - **Settings namespace migration.** Settings state, backend catalogs, help rendering, and provider configuration now use a private scope with live compatibility accessors and a `window.TTVC.settings` API. - **Character library namespace migration.** SQLite-backed character persistence, identity merging, image updates, and rendering now use a private scope with explicit compatibility exports and a `window.TTVC.characters` API. - **Studio namespace migration.** Four-phase navigation, DOM borrowing, resume state, and cast-footer coordination now use a private scope with explicit compatibility exports and a tested `window.TTVC.studio` surface. - **Rehearser cast namespace migration.** Cast view rendering, filtering, sorting, and delegated controls now use a private scope with explicit compatibility exports and the existing cast namespace. - **Rehearser voice-design namespace migration.** Character development and backend-refresh controls now use a private scope with explicit compatibility exports and the existing voice-design namespace. - **Rehearser voice-picker namespace migration.** Local matching, preview state, Fish.audio search/import, and candidate assignment now use a private scope with explicit compatibility exports. - **Rehearser stage namespace migration.** Stage scaling, cast-sidebar controls, and strip rendering now use a private scope with explicit compatibility exports and the existing stage namespace. - **Audiobook network namespace migration.** Timeout, streaming, and attribution transport helpers now use a private scope with explicit compatibility exports and the existing Audiobook namespace. - **Rehearser library namespace migration.** SQLite persistence and the IndexedDB migration adapter now use a private scope with explicit database compatibility exports and the existing library namespace. - **Reader namespace migration.** PDF/TXT document state, playback, search, synthesis, and library APIs now use a private implementation scope with explicit compatibility exports and a tested `window.TTVC.reader` surface. - **Audiobook parser compatibility boundary.** Speech-heuristic constants consumed by the scoped Audiobook core are now exported explicitly, preventing load-order regressions while preserving the parser namespace. - **Audiobook core namespace migration.** Reader-side audiobook state, attribution helpers, quote controls, and Rehearser handoff now use a private scope with explicit compatibility exports and a regression-tested core namespace. - **Rehearser core namespace migration.** Script state, pagination, cast-management, and summary APIs now live inside a private core scope with explicit legacy exports and a regression-tested `window.TTVC.rehearser.core` surface. - **Rehearser recording namespace migration.** Microphone overlay, recording controls, and shared microphone errors now use a private implementation scope with explicit compatibility exports. - **Rehearser I/O namespace migration.** Serialization, library persistence, import/export, and PDF helpers now use a private implementation scope with explicit compatibility exports and the existing namespace API. - **Rehearser playback namespace migration.** WebAudio transport, decoded-buffer caching, line-audio caching, and batch synthesis now use a private implementation scope with explicit compatibility exports and a tested namespace API. - **Stateful Audiobook namespace migration.** Casting/recast orchestration and preview/export now keep internal helpers private and publish their public compatibility surface explicitly through `window.TTVC.audiobook`. - **Pure leaf namespace migration.** Rehearser parsing/training and Audiobook parsing/segmentation helpers now keep declarations private and publish explicit compatibility exports alongside their `window.TTVC` APIs. - **P1 architecture audit.** Marked the incremental frontend modularization complete: all planned Audiobook/Rehearser responsibility slices are manifest-driven, namespace-tested, and compatibility-preserving; full ES-module migration remains future work. - **Rehearser training extraction.** Training-mode state, cue playback, transcription, microphone recording, and phase controls now live in `rehearser-training-ui.js`. - **Production bundle refresh.** Rebuilt `static/dist/main.min.js` from the current manifest so the release path includes the validated frontend fixes and retry-history behavior. - **Rehearser recording extraction.** Microphone recording, level metering, overlay state, and recorded-line controls now live in `rehearser-recording.js`. - **Audiobook preview/export extraction.** Editable cast review, Rehearser handoff, chapter-aware synthesis/export, pause settings, and export browsing now live in `audiobook-preview-export.js`. - **Audiobook casting-pipeline extraction.** LLM casting, unknown-speaker recast, automatic repair, and cast-view orchestration now live in `audiobook-pipeline.js`. - **Rehearser I/O extraction.** Session serialization, library save/load/autosave, REH/Fountain/FDX/OSF export, and PDF/session import now live in `rehearser-io.js`. - **Rehearser playback extraction.** WebAudio playback, transport controls, bounded decoded-audio prefetch, persistent line-audio caching, and synth-all controls now live in `rehearser-playback.js`. - **Audiobook namespace wiring.** Network helpers and public casting, recast, export, and save actions now register after their lazy modules load, completing the namespace bridge without removing legacy global entry points. - **Audiobook live cast-view extraction.** The live cast feed, roster, settings, and prompt controls now live in `audiobook-cast.js`; the legacy `audiobookCastView` entry point remains available for compatibility. - **Rehearser voice-matching extraction.** Local voice matching, preview-line selection, fish.audio search/import, and candidate assignment now live in `rehearser-voice-picker.js`; DOM event registration remains timing-safe. - **Audiobook/Reader stylesheet extraction.** Reader controls, Audiobook casting, page layout, character overlays, and confidence/recast styles now live in `static/css/audiobook.css` with release cache busting. - **Rehearser stylesheet extraction.** The main Rehearser layout, cast, stage, training, library, and script-view rules now live in `static/css/rehearser.css`, loaded after the core stylesheet with release cache busting. - **Rehearser cast-renderer extraction.** The full cast-card renderer and delegated cast interactions now live in `rehearser-cast.js`; the legacy `renderCastList` global and existing behavior remain available. - **Provider status clarity.** TTS and STT backend discovery now reports configuration separately from liveness, with bounded endpoint/status/error details shown in backend help text so unreachable services are actionable. - **Audiobook cast normalization extraction.** Same-speaker post-cast merging now lives in `audiobook-cast.js`, including dialogue-only merging, split-sentence repair, and nearby-duplicate removal, phantom-speaker filtering, and speaker-alias consolidation, keeping deterministic segment cleanup separate from live casting orchestration. - **Audiobook segmentation extraction.** Quote segmentation, conservative turn-taking, and dialogue-piece validation now live in `audiobook-segmentation.js`; the live casting pipeline remains compatible with the same global helpers. - **Rehearser voice-design extraction.** Character development, backend selection, and backend-refresh synchronization now live in `rehearser-voice-design.js` while preserving the existing cast controls and global API. - **Voice Design stylesheet split.** Voice-design form, sample-grid, and preview-match rules now live in `static/css/voice-design.css`. - **Background-job stylesheet split.** Job history and retry-panel rules now live in `static/css/background-jobs.css`. - **Resource diagnostics stylesheet split.** Performance-panel styles now live in `static/css/resource-diagnostics.css` and are loaded with the same release cache-busting version as the core stylesheet. - **Audiobook cast export extraction.** Markdown serialization and legacy page-stamping migration now live in `audiobook-cast.js`, leaving the live casting workflow in `audiobook.js` and preserving the same global helpers. - **Rehearser training extraction.** Transcript normalization, LCS matching, and word-level comparison now live in `rehearser-train.js`; recording and state-machine wiring remain behavior-compatible in `rehearser.js`. - **Rehearser cast-controls extraction.** Cast view switching, sorting, filtering, and their guarded event wiring now live in `rehearser-cast.js`, reducing the main renderer's responsibility while preserving the legacy API. - **Namespace migration surface.** Extracted Rehearser and Audiobook modules now publish parse, stage, voice-picker, and persistence APIs through `window.TTVC` while retaining legacy globals for the ongoing migration. - **CI frontend gate.** The manifest-driven JavaScript syntax and top-level declaration collision check now runs in the SonarQube quality workflow, alongside Python compilation, regressions, and the production bundle build. - **Structured request correlation.** API and MCP requests now receive a bounded `X-Request-ID` and emit sanitized method/path/status/duration logs without recording request bodies or secrets. - **Audiobook parser extraction.** Dialogue detection, PDF dehyphenation, and shared speaker-heuristic constants now live in `audiobook-parse.js`, loaded before the Audiobook casting workflow without changing its legacy API. - **Rehearser voice-picker extraction.** Voice preview state and matched voice-choice presentation now live in `rehearser-voice-picker.js`; matching, import, and assignment actions remain compatible with the existing cast API. - **Rehearser stage extraction.** Stage font scaling, cast-sidebar collapse state, cast filtering/sorting, and cast-strip rendering now live in `rehearser-stage.js`, reducing the main Rehearser module while preserving its legacy globals. - **Release cache-busting.** Bumped the app version and HTML stylesheet/loader URLs to `1.24.25` so the extracted module manifest and current frontend assets are not hidden behind stale browser caches. - **Rehearser library extraction.** SQLite persistence helpers and the one-time IndexedDB migration now live in `rehearser-library.js`, loaded with the Rehearser/Reader/Library lazy groups; behavior remains global-compatible. - **Expanded resource diagnostics.** The Performance panel now reports bounded browser cache counters, container cgroup current/max memory, artifact-cache bytes, and sanitized largest recent operation durations. - **Current-release constrained validation.** Built `1.24.21` and served it under a hard 4 GiB cgroup limit; startup remained healthy at 52.54 MiB, 0.10% CPU, 6 PIDs, and no OOM kill. - **Release cache-busting.** Updated the HTML app-version, stylesheet URL, and loader URL to match `VERSION` (`1.24.21`) so deployed browsers fetch the current core assets. - **Lazy-navigation race protection.** A restored-section callback can no longer replace a newer user navigation request while section-specific modules are still loading, including under CPU throttling. - **Constrained workload coverage.** Added a 4× CPU-throttled 500-character browser workload regression that records bounded DOM/audio/heap/image-cache metrics while preserving pagination. - **Bounded library image lifecycle.** Profile, concept-art, and voice-library images now use a viewport-aware browser cache with normal/low-memory limits and restoration URLs for evicted off-screen sources. - **Shared provider-health probes.** TTS, STT, and container connectivity checks now use one bounded helper with normalized connection, timeout, HTTP-status, and JSON-shape handling while retaining provider-specific endpoints. - **Section-scoped feature loading.** The shared module manifest now builds a 17-module foundation bundle and loads Rehearser, Reader/Audiobook, Library/Characters, and Studio code only when their sections are opened. Repeated navigation reuses loaded modules, while deep links load their group before activation. - **P1 Roadmap status.** Documented the completed namespace/manifest foundations, section-scoped lazy loading, durable background artifacts, provider-health consolidation, workload measurements, image-cache eviction, and the first Rehearser responsibility extraction; remaining frontend splits stay explicitly open. ### Added - **Bounded Rehearser Audio Cache.** Decoded/synthesized line audio now uses a 128-entry LRU cache so long scripts cannot grow browser memory without limit. - **Shared Voice-Library Cache.** Library, Benchmark, and Rehearser now share a bounded session cache with in-flight request coalescing and explicit invalidation after voice mutations. - **Frontend Namespace Bridge.** A stable `window.TTVC` migration surface now exposes Audiobook, Rehearser, and job APIs while preserving legacy globals for compatibility during the larger module split. - **Book 2 Attribution Generalization.** The full 1,663-segment Book 2 snapshot is now a regression; structural `Narrator` labels can no longer become dialogue speakers or phantom cast members. - **Retry UI Regression Coverage.** Desktop smoke tests now verify that a Retry button is rendered only when the authenticated job history explicitly advertises a safe live retry and that the retry endpoint is invoked once. - **Safe Background Retries.** Failed live transcription, voice-design, casting, and consistent-image jobs now expose bounded retry actions from stripped in-memory inputs; retry payloads are never returned or persisted and expire on restart. - **Canonical Pair Asset Digests.** Generated profile/reference pairs now retain SHA-256 asset digests in character metadata, and visual-pair review reports either asset being replaced after generation. - **Live Image Result Preservation.** Durable image artifact persistence now leaves the active in-memory pair available to the current browser poll while storing only bounded download metadata for restart-safe history. - **Resource Diagnostics.** Performance now exposes read-only server RSS, configured/temp disk usage, persisted-job count, DOM/audio counts, and browser heap when available; host Docker metrics remain explicitly outside the panel's scope. - **Throttled Desktop Smoke Coverage.** Chromium CDP now exercises app navigation at 4× CPU throttling and asserts bounded DOM/audio counts while capturing the browser heap metric when available. - **Background Job History Panel.** The Benchmark section now shows sanitized, restart-safe background-job status, progress, elapsed time, errors, and cancellation controls. Replay is deliberately limited to reopening the source workflow because prompts, text, audio, and image inputs are never retained for implicit retries. Completed voice-design audio and consistent-image outputs are now stored as bounded atomic artifacts with authenticated download links. - **Protected Backup Recovery.** Added an authenticated backup inventory and restore API for character and rehearsal snapshots, with path-safe file selection and atomic pre-mutation backups. - **Voice Trash Recovery.** Voice deletes and group deletes now expose a path-safe trash inventory and restore API, preserving the complete voice package and its enabled/hidden state. - **Explicit Database Migrations.** SQLite now records schema version 1 and applies additive legacy-column migrations at startup; a generation-history table is available for reversible prompt/result auditing. - **Character and Voice Metadata Validation.** Added bounded type and size validation before records and voice sidecars are persisted. - **Character Appearance Forms.** A character can now keep alternate transformations or disguises in bounded `sheet.forms[]` entries, with one-character/one-voice attribution, a detail-page form switcher, selected-form prompt resolution, and per-form portrait/reference assets. ### Changed - **API Key Handoff Cache Policy.** Settings and explicit external-key regeneration responses now send `no-store` headers so browser and intermediary caches cannot retain the client-visible access-key handoff. - **Navigation Race Protection.** Rehearser phase requests now survive late feature-module initialization, and overlapping character-library refreshes cannot overwrite a newer table/card view. - **Constrained Browser Smoke Coverage.** Added a 4 GB `navigator.deviceMemory` regression that verifies shared batch work is capped at two concurrent operations. - **Optional Background Transcription.** `/api/transcribe` now accepts `background: true` and exposes bounded job status/cancellation endpoints with timeout and failure states; existing synchronous callers remain compatible. - **Optional Background Voice Design.** `/api/voice-design` now accepts `background: true` and exposes bounded job status/cancellation endpoints with queue state, timeout and failure handling; existing synchronous callers remain compatible. - **Optional Background Casting.** `/api/match-characters-voices` now accepts `background: true` and exposes bounded job status/cancellation endpoints while reusing the existing casting validation, retries, and idempotency behavior. - **Attribution Fixture Coverage.** Added a neutral multi-speaker Book 2-style fixture regression to verify parser normalization across named speakers and narration without requiring a live provider. - **Visual Pair Review Workflow.** The character library now exposes the read-only visual-pair audit directly in its toolbar and displays actionable per-character findings without changing saved assets. - **Durable Background Job Metadata.** Transcription, voice design, casting, and consistent-image jobs now persist bounded status metadata in SQLite; unfinished jobs are reported as interrupted after a server restart, while prompts, secrets, audio, and image payloads are excluded from persistence. - **Bounded Durable Job Retention.** Completed background-job metadata is pruned oldest-first at a fixed limit so restart-safe status tracking cannot become an unbounded database growth path. - **Frontend Quality Gate.** Added `npm run check:frontend` to validate every manifest module for syntax errors, duplicate manifest entries, and top-level declaration collisions before bundling. - **Provider Boundary Regression Coverage.** Added deterministic tests for STT backend normalization and ComfyUI workflow submission, prompt injection, polling, seed derivation, and image retrieval. - **Casting Constraint Coverage.** Added deterministic regression coverage proving casting filters invented voice IDs and avoids unnecessary duplicate assignments when distinct catalogue voices are available. - **Attribution and Timeout Safety Coverage.** Added regressions preventing phantom narrator speakers, preserving source prose after malformed LLM JSON, clamping caller timeouts, and bounding upstream error text. - **Unified Provider Retries.** TTS and LLM provider requests now retry bounded connection, timeout, and transient HTTP failures with exponential backoff while closing discarded responses. - **Constrained DOM Coverage.** Added a 250-character browser fixture proving character-library rendering stays within the configured 50-row page and expands only on explicit user action. - **Sanitized Job History API.** Added bounded `/api/jobs` and `/api/jobs/{job_id}` endpoints for recent restart-safe job metadata, with fingerprints and sensitive/binary payloads excluded. - **4 GiB Container Validation.** Verified the current image serves the application under a hard 4 GiB cgroup limit with no OOM kill; the probe recorded approximately 50.6 MiB idle memory and 0.09% CPU. - **Profile Completeness Coverage.** Added browser regression coverage for empty, partial, and fully populated character-sheet counts used by the library table and profile sorting. - **STT Cancellation Safety.** Queued transcription cancels before consuming the single STT slot, and timed-out requests keep the device slot held until the underlying worker actually exits. - **Credential Boundary Review.** Provider credentials are now documented as server-only; only the explicitly regenerated external caller key crosses to the UI, with no-store delivery and no browser-storage persistence. - **API Boundary Hardening.** External API authentication is enabled by default, request and mutation limits are enforced, and settings responses redact provider credentials for non-browser callers. - **Server-side Provider Credentials.** Provider keys are now masked even for same-origin Settings responses, are preserved when masked/empty fields are saved, and model/health probes resolve configured keys inside the server. Browser localStorage is no longer used as a credential store. - **Image Job Deduplication.** Consistent portrait/reference generation accepts an `Idempotency-Key` and returns the existing job for repeated submissions; completed job mappings are evicted with the job cache. - **Image Concurrency Bound.** Consistent image generation is limited to two active GPU jobs; additional requests remain observable as queued jobs and fail clearly if no slot becomes available within 15 minutes. - **Fish.audio Cache Eviction.** Offline search responses are now evicted by configurable file-count and byte-size limits, preventing an unbounded cache from consuming the configuration volume. - **Docker Socket Isolation.** Compose deployments now place the host Docker socket behind a dedicated network proxy; the web container no longer mounts the socket or joins the host Docker group. The client retains direct Unix-socket compatibility for deployments that intentionally configure it. - **TTS Queue Bound.** Preview, style-variation, seed-finder, and OpenAI-compatible speech-proxy synthesis now share a two-slot process-local queue; excess requests receive a retryable 429 after a bounded wait instead of creating unbounded worker threads. - **STT Queue Bound.** Transcription and STT benchmark calls now share a single bounded process-local slot, preventing concurrent recognition requests from overcommitting the inference device. - **Visual Identity Versioning.** Consistent profile/reference results now carry the standardized v3 prompt marker, and older unmarked image pairs are visibly flagged for regeneration. - **Visual Identity Fingerprints.** Consistent pairs now record a compact fingerprint of canonical appearance fields; changing those fields causes the existing pair to be flagged stale. - **Character Table Pagination.** Productions now render 50 character cards/rows at a time with an explicit Show more control, keeping sorting and record lookups intact while reducing DOM and image work for large casts. - **Voice Table Pagination.** The voice library now renders up to 50 sorted rows initially and adds a Show more control for larger result sets, while retaining the full dataset for filtering and sorting. - **Upload Filename Boundaries.** Audio, STT, route-sound and picture uploads now reject path-like or control-character filenames before deriving extensions; voice ZIP imports also reject absolute, traversal and malformed archive paths. - **Generation Audit History.** Prompt and image generation now record bounded metadata in the protected history API; prompt/source content and sensitive result fields are excluded from the audit record. - **Synthesized Audio Cache Eviction.** Persisted line-audio clips now use configurable global file-count and byte limits with oldest-first eviction, and oversized individual clips are rejected. - **Voice Design Idempotency.** Repeated voice-design submissions with the same `Idempotency-Key` now reuse the original temporary result; reusing a key for different request data is rejected. - **Casting Idempotency.** Character-to-voice matching now applies the same bounded, expiring idempotency contract, preventing repeated clicks from issuing duplicate LLM casting requests. - **Image Job Cancellation.** Consistent profile/reference jobs now expose a cooperative cancellation endpoint and progress-panel Cancel control; queued work can be cancelled without interrupting unrelated ComfyUI jobs. - **Low-Memory Table Mode.** Character and voice-library pages now render 25 rows at a time on devices reporting approximately 4 GB RAM or less, while images remain lazy-loaded. - **Visual Pair Review Warning.** Character details now flag profile/reference pairs when consistency metadata is missing, stale, changed after profile edits, or explicitly marked inconsistent—even when the prompt version is current. - **Visual Pair Audit.** Added a read-only character-library review endpoint that detects incomplete pairs, reused or identical profile/reference assets, legacy metadata, changed appearance fields, and explicitly inconsistent pairs. - **Shared Frontend Module Manifest.** The fallback loader and production bundler now consume the same module manifest, preventing the two loading paths from drifting as the frontend is split further. - **Preview Blob Lifecycle.** Character-library and Rehearser local voice previews now revoke replaced or completed object URLs, reducing retained browser memory during repeated auditioning. - **Character Library Smoke Coverage.** Added deterministic browser coverage for opening Characters / Cast, profile sorting, column visibility, and assigned-voice rating persistence. - **Attribution Quote Repair.** Mixed German guillemet pairs are now handled by the deterministic fallback, and normalized LLM segments are repaired when an epigraph is split before its closing quote. - **Attribution Regression Coverage.** Added focused tests for mixed guillemets and the Schwester Larissa epigraph split regression. - **Security Regression Coverage.** Added a Python test gate for character metadata validation, private-download URL rejection and oversized JSON request rejection. - **Navigation Reliability.** Rehearser phase requests now survive the feature-module initialization boundary. - **Container Hardening.** The web container now uses a read-only root filesystem, drops all Linux capabilities, and enables `no-new-privileges`; application data remains writable only through the declared mounts. ### Fixed - **SSRF and Credential-Leak Paths.** User-supplied downloads are validated against private-network access settings, and provider keys no longer travel in model-probe URL query strings. ## [1.22.3] — 2026-09-04 ### Changed - **P0 Request Protection.** Added HTTP request-size ceilings, reduced the default upload limit from 1 GB to 256 MB, and added process-local mutation rate limiting with stricter limits for destructive paths. - **Credential Transport.** Provider API keys used for model probes are now sent in request headers instead of URL query strings, keeping them out of browser history and ordinary access/proxy logs. - **Recoverable Character and Rehearsal Changes.** Character updates/deletes and rehearsal recasts, updates, compaction and deletes now create atomic JSON snapshots before mutation. Missing delete targets return 404 instead of success. - **SSRF Protection.** User-supplied audio imports, picture URLs and generic screenplay URLs now use centralized URL validation, with private-network access available only through the explicit local-download setting. ## [1.22.2] — 2026-09-03 ### Added - **SonarQube Continuous Analysis.** Added `.github/workflows/sonarqube.yml` and `sonar-project.properties` so pushes to `main` are scanned by the SonarQube Community Build instance, with the Quality Gate blocking the job on failure. The scan covers hand-written source only — the generated `static/dist/` bundle, vendored code, ComfyUI workflow graphs and runtime `config/` state are excluded, since scanning them buries real findings in noise. CI verifies that Python compiles, every JavaScript file parses and the bundle builds; the Playwright suite is deliberately not wired in yet because it drives a running application on port 7890 and cannot execute on a bare runner, and a step that always fails would make the Quality Gate meaningless. Credentials are referenced only as `secrets.SONAR_TOKEN` / `vars.SONAR_HOST_URL` and appear in no committed file. ### Changed - **Deployment Now Uses a Published Image.** The container image was only ever built locally and never pushed anywhere, so Portainer's "Pull and redeploy" always failed with `pull access denied … repository does not exist` — and the failed deploy removed the running container, taking the app offline. The image is now published to GitHub Container Registry as `ghcr.io/martin-b78/tts-voice-creator-clone-and-design-2`, tagged both `:latest` and the current version, and `docker-compose.yml` / `portainer-stack.yml` reference that name, so re-pull works normally. `docker-compose.yml` keeps its `build:` section, so `docker compose build` still produces the image locally under the registry name. Python and static files remain bind-mounted: code changes still need only a container recreate, and a rebuild plus push is required only when `Dockerfile` or `requirements.txt` change. ### Fixed - **Book Period Was Never Applied to Character Images.** `csBuildImagePrompt` returned the LLM-written `image_prompt` from its first line, before the block that appends the book's genre, setting and era — so whenever an LLM prompt existed (effectively always) the world grounding was dead code. That block even contained an explicit rule that a rank title "should NOT be drawn in a contemporary military uniform", which therefore never ran: "Oberst Alrik" was rendered in a 19th-century dress uniform with epaulettes and gold braid in a book explicitly dated 1420-1500. The grounding is now appended to LLM-written prompts as well, and `/api/character-generate-image` accepts a `book_context` field so direct API callers get the same period lock. Verified by regenerating: epaulettes and the Napoleonic tunic are gone. - **Portraits Were Full-Body Scenes, Not Casting Portraits.** Generated `image_prompt` values narrated the scene the character appeared in — "standing in a grassy landscape at dusk", "he is riding a horse", "the background shows a campfire" — so every portrait came out as a wide environmental shot, and one rendered a medieval character in a room with a radiator and a modern window. Three changes: the sheet guidance now forbids setting, location, weather, background and actions in `image_prompt` (not just art style); the portrait path strips whole scene sentences from the ~190 prompts already saved; and the appended rendering directive now states framing explicitly and overrides the prompt — one person, tight head-and-shoulders, plain neutral backdrop, no anachronistic objects. Verified by regenerating: framing is tight and the invented clutter is gone. - **Injury and Transformation Guidance Strengthened Again.** The first fix (baseline appearance, wounds to `notes`) was not enough: a character regenerated *after* it still produced "naked, almost flayed, raw flesh dripping blood" as their permanent description, and rendered as a nude gore figure. The field now also names gore, nudity, flaying and transformation, and instructs that when the book only ever describes someone vividly during such a scene, their ordinary clothed appearance should be inferred from role, rank and origin and marked as inferred. - **Portrait Generation Produced Pure Black Images.** The bundled portrait workflow ran Flux-dev with `cfg: 3.5`. Flux-dev is a distilled model and requires `cfg: 1.0` (its guidance belongs in a `FluxGuidance` node), so the sampler produced a black frame — which ComfyUI reported as a *successful* job, and the app saved as a 19 KB "portrait". Every other bundled workflow (concept sheet, identity, consistent-images) already used `cfg: 1.0`; the portrait graph was the lone outlier. Corrected to 1.0, after which the same prompt renders a real 1.28 MB image. - **Bundled Workflows Are Baked Into the Image, Not Mounted.** `/app/workflows` ships inside the container, so editing `workflows/*.json` in the repo has no effect at runtime until the image is rebuilt — the container kept reporting the old `cfg: 3.5` after the file on disk read `1.0`. The corrected graph is therefore also saved into Settings (`comfyui_workflow`), which takes precedence over the bundled copy and persists across restarts. - **Character Portraits Rendered People's Injuries as Their Faces.** The `physical` field captured whatever the book described at a character's worst moment — Bartka's read "Schädel ist eine blutverkrustete Masse, skalpiert", Amber's "trägt eine klaffende Wunde an der Brust" — and that text is used verbatim to generate the portrait, so Bartka rendered as a half-skull corpse and Amber with a bleeding chest. The field now explicitly asks for the character's baseline appearance through most of the book, with wounds, blood, dying and unconscious states directed to `notes`/`arc_note` instead, and states why: a transient injury written there becomes the character's permanent face. - **Portrait Prompts Fought Their Own Rendering Directive.** Saved `image_prompt` values embedded their own style, medium, framing and background ("painterly game concept art", "plain neutral parchment background"), while the renderer appends a directive demanding a photorealistic live-action portrait that is explicitly "not an illustration, painting". Every portrait was generated from two contradictory briefs at once. The field guidance now forbids style/medium/framing/ lighting in `image_prompt`, and the portrait path strips such clauses from the ~190 prompts already saved, keeping only the text describing who the character is. Concept-art generation is untouched, since it has its own illustrative directive. - **Buttons Silently Did Nothing While a Pass Was Running.** Casting, Recast, Verify and Export all opened with an `if (_audiobook.running) return;` guard that returned in complete silence, so clicking one while another pass was in flight looked identical to a broken button. This window opens on its own: casting auto-starts the auto-repair sequence 800 ms after finishing, with no visible indication. Confirmed live — an automated pipeline recorded Recast and Verify as "done" in under a second each because both returned instantly this way, which would have reported a cast-only result as a full pipeline. The guards now explain the refusal instead of swallowing the click. - **ElevenLabs Imports Always Tagged EN.** `sourceLanguageCode()` required an uppercase 2-letter code to recognize an already-valid language code, but ElevenLabs' API returns it lowercase ("fr", "de") — the check silently failed and fell back to a name-based lookup table that only has full language names as keys, not ISO codes, defaulting every non-English ElevenLabs import to EN regardless of the voice's actual (correctly displayed) language tag. Now matches the code case-insensitively. - **Voice-Clone Reference Transcript Never Reached the TTS Engine.** Saving a reference transcript from the Voice Library only wrote this app's own sidecar file — the actual Qwen voice-clone engine reads `ref_text` from its own separate `voices.json`, which nothing updated outside one unrelated batch-import code path. No amount of restarting TTS containers fixed a design-origin voice's playback (`ref_text is required` / connection reset) because the stale `voices.json` entry itself never changed. Saving a transcript now also publishes it to the engine and clears the stale cached embedding. - **Rename Filename Silently Did Nothing.** Same root cause as an earlier `.vr-bench` bug: selecting a voice moves its list row into the single-voice detail view, so by the time "rename filename" was clicked there, the code's `wrap.querySelector('.vr-name-input')` was searching an already-emptied container and found nothing — no error, no rename. Added the same `#voices-inspector` fallback already used for the benchmark cell, plus a visible error if the control still can't be found. - **Design-Origin Voice Playback Permanently Misrouted.** A voice's Play button decided voice_clone vs Voice Design backend by checking `origin !== 'designed'` — so a voice originally made via Voice Design that had since had a real reference WAV + transcript attached (fully clone-capable) still had every playback routed to the Voice Design service forever, 404ing when that service wasn't reachable at its configured port. Now decided purely by whether the voice currently has a reference clip. - **STT Engine Picker Had No Effect.** Picking a different engine in the footer's STT flyup only pushed the value into a few unrelated `<select>` elements on other pages — it was never persisted, and `/api/transcribe` (used by "Re-recognise text") always fell back to the legacy, frequently-dead "Configured Whisper/STT" URL regardless of what was selected. The flyup now persists the pick to the (previously unused) `stt_preferred_backend` setting, and `/api/transcribe` honors it when no explicit backend is requested. - **Design-Origin Voices Permanently Misrouted for Playback.** A voice's Play button decided whether to use the voice_clone or Voice Design backend by checking `origin !== 'designed'` — so a voice originally made via Voice Design that had since had a real reference WAV + transcript attached (making it fully clone-capable) still had every playback routed to the Voice Design service forever, producing a 404 when that service wasn't reachable at its configured port. Now decided purely by whether the voice currently has a reference clip. ### Added - **Attribution Confidence + Review Navigation.** Every cast line now carries a colour-coded, calibrated confidence badge — `Confident 96%`, `Likely 79%`, `Likely 75%`, `Probably 69%`, `Doubtful 47%`, `Guess 16%` — with a tooltip giving the reasoning ("no nearby speech tag", "speaker is a generic role label", "fused: several turns in one segment"). Scored from local evidence only, no extra LLM calls. The percentages are **calibrated against the hand-corrected reference rather than raw heuristic points**: each band displays the correctness actually observed for lines scoring in it, so "Doubtful 47%" means such lines really were right 47% of the time. Every band verified within 0.8 points. Nothing is labelled "certain" — even the best band is wrong about 1 line in 25, and a label the reviewer learns to distrust is worse than no label. Review controls sit in the feed toolbar: prev/next buttons that jump only between lines needing attention, a live count, a threshold selector ("Not Confident" or "Doubtful + Guess"), and F2 / Shift+F2. **On the measured book this flags 1405 of 2273 lines (62%) while catching ~90% of all real attribution errors** — a substantial saving over reading everything, though less dramatic than the 45% suggested by the text-matched subset alone. Also adds "2.6 Score attribution confidence" under Identify Characters, which scores an existing cast in place (instant, no LLM calls) so books cast before this feature don't need re-casting to get colours. - **Generic Role Labels No Longer Get Character Profiles.** Bit parts labelled only by role ("Wache", "Mann", "Menge") that speak a line or two cannot support a profile — there is nothing in the book to fill one from. Measured: 85% of generic-role-label records came back completely empty (24 of 28) versus 13% for records naming a real person. Role labels with fewer than 4 lines are now skipped for profile generation, saving an LLM call each and keeping empty shells out of the character library. They remain in the cast and still get voices; role labels with real presence (4+ lines) are still profiled. - **"Also Known As" Role-Label Resolution.** The caster routinely credits a line to what a character *is* ("Schmied", "Henker", "Druide") rather than who they *are*, and each such label became its own cast entry with its own voice. Speaker labels are now resolved to proper names by reading German appositive constructions out of the book's own text — "Darrag der Schmied" / "der Schmied Darrag" / "Zerwas den Henker" — where the role is always the noun immediately after the article. Purely grammatical, so it carries to any book with no per-title configuration. Requires the pairing to recur at least twice and win outright, since one stray capitalised-noun pair is otherwise enough to bind a role to the wrong character. Measured against the hand-corrected reference: `Schmied→Darrag`, `Druide→Gamba`, `Amazone→Lysandra`, `Henker→Zerwas`, +35 correct attributions. ### Fixed - **Regional Accents Use BCP 47, Eliminating Code Collisions.** English voices can be named for their accent — `EN-GB`, `EN-US`, `EN-AU`, `EN-CA`, `EN-IE`, `EN-SCT`, `EN-NZ`, `EN-ZA`, `EN-IN` — with the same pattern for `DE-AT`, `DE-CH`, `PT-BR`, `ES-MX`, `ES-AR` and `FR-CA`. All collapse to their base language for routing and detection, since text alone cannot distinguish Australian from Irish English. Expressing the region as a BCP 47 subtag rather than a bare country code removes the namespace clash outright: every bare code keeps its ISO 639-1 meaning, so `CA` is Catalan, `AR` Arabic, `BR` Breton, `UK` Ukrainian, `IE` Interlingue, `CH` Chamorro, `ZA` Zhuang, `SG` Sango and `CO` Corsican. An interim country-code scheme had mapped nine of those onto the wrong language — `AR`→Spanish would have silently routed every Arabic request to a Spanish voice. Only country codes that are *not* ISO 639-1 languages (`GB`, `US`, `AU`, `NZ`, `PH`, `AT`, `MX`, `QC`…) are still accepted, purely so existing routes and the library's `PH_*` voice keep working. - **One Language Picker Everywhere.** The searchable flag dropdown is now used by every language selector — Clone a Voice, Design a Voice, the voice library's Add dialog, the Rehearser's design language and App Routing — instead of each having its own hardcoded and differently-sized list. Pickers that feed the TTS backend a language *name* ("German", not "DE") are enhanced without changing their values, so that contract is preserved. - **Non-ASCII Names Were Being Destroyed.** Route app/voice/sound values were sanitised against an `A-Za-z0-9` whitelist, so every non-ASCII letter became an underscore: an app named "Hörbuch" was stored as "H_rbuch", and Cyrillic, Greek, Japanese or Filipino names were reduced to underscores, after which the route could never match the name the user had typed. Sanitising now uses Unicode word characters, and the routes file is read and written as explicit UTF-8 (stored unescaped, so it stays human-readable). Punctuation and control characters are still stripped. Verified by round-trip through the API and disk. - **Language Detection Widened From 8 To 28 Languages.** Routing's text detector recognised only EN/DE/FR/ES/IT/PT/NL/PL, so a rule for any other language could never match on detected text — including the Filipino voice in the library. Non-Latin scripts are now identified by writing system (Cyrillic, Han, Kana, Hangul, Arabic, Hebrew, Greek, Devanagari, Thai and more), which needs no vocabulary and covers those languages permanently; Latin-script coverage gained Filipino, Indonesian, Turkish, Swedish, Danish, Norwegian, Finnish, Romanian, Czech, Hungarian, Vietnamese and Catalan. `PH`/`FIL` now alias to `TL`, and `GB`/`US` to `EN`, so a route labelled with a country code still matches. Fixed three defects found while testing: ä/ö/ü scored +3 for German although Turkish, Swedish and Finnish use them too ("bugün nasılsın" and "tack så mycket för hjälpen" both came back as German — now +1, with genuinely unique marks like ß, ñ, å keeping the strong score); Dutch, Polish and Portuguese sat in the score table with no word list at all and could never win on vocabulary; and a matched word scored the same as a shared diacritic, so one real hit only tied and lost on dict ordering. Verified 28/28 on a language sample. - **Routing Silently Reverted Unsupported Languages.** `core/routing.py` validated a route's language against its own eight-value whitelist and reset anything else to "Any" on save — so choosing Filipino, picking Save, and watching the row snap back to Any looked like a UI bug but was the server discarding the value, with no error. Any ISO-style 2-3 letter code is now accepted; genuinely malformed values still fall back to "Any". Verified by round-trip: TL persists, garbage does not. - **App Routing Covers Every Language.** The routing rules' Language dropdown offered only eight languages plus Any/Auto, so a route could not be written for anything else — a Filipino route had to be left on "Auto". It now draws from the same shared 79-entry language list, with Any/Auto still first. Deliberately kept as a `<select>` rather than the free-text combobox used for voice naming: a route's language is matched exactly against the detected code, so a typo there would not look broken — the rule would simply never match and traffic would fall through to a different voice. Falls back to the original eight if the shared list is unavailable. - **Any Language Code Can Be Entered.** The Language pickers in Clone a Voice, Design a Voice and the voice library's Add dialog were `<select>`s with ~17 hardcoded codes, so a language outside that list simply could not be chosen — reported live for Filipino, which had no entry. They are now editable comboboxes backed by a shared searchable datalist of 77 languages (code plus name, so typing "Tagalog" finds `TL`), and any code not on the list can still be typed directly. Input is constrained to letters, upper-cased and capped at three characters, since the code becomes part of the voice's filename ID. The generated ID now also updates while typing rather than only on blur. - **Arrow-Key Review Workflow.** Reviewing 1400 flagged lines by mouse was impractical, so the review queue is now driven from the keyboard: **←** previous, **→** next, **↑** mark correct, **↓** mark wrong, **1–9** assign the Nth most-spoken character, **n**/**0** assign Narrator, **u** undo. Marking or assigning settles the line and advances automatically, so a triage pass never re-serves the same line. Correct and wrong are separate verdicts: flagged-wrong lines drop into their own "Marked wrong" filter for a second, fixing-only pass, which is far quicker than deciding and repairing line by line. The counter shows both (`847 to check · 412✓ 144✗`), verdicts persist with the cast draft, and plain arrows are only captured once a review is under way so ordinary scrolling still works. Also fixes the F2 shortcut, which was bound to a `<div>` and therefore never fired unless that element happened to hold focus. - **Same-Speaker Dialogue Never Re-Merged After Recast.** The merge pass runs once at the end of casting, but recast and verify then reassign speakers and create fresh runs of consecutive lines by one character that nothing ever merged — 213 of them in the measured book. A dialogue-only merge now runs after those passes. Deliberately one-sided: measured against the human reference the caster's narration is already too coarse (median 152 chars vs 123) while its dialogue is slightly too fine (62 vs 65), and merging narration as well pushed text mismatches *up* from 727 to 746. Dialogue-only brings them down to 588 and moves the segment count from 2273 to 2059 against the human's 2110. Emotion tags are deliberately not required to match — requiring them blocked all but 42 of the 214 merges and left segmentation at 2230 segments / 685 mismatches. The cost is one emotion tag per character block rather than per chunk; the hand-corrected reference merges these blocks too, so a human reading the scene judged one emotion sufficient. - **Speaker Attribution Biased Toward the Protagonist.** The casting prompt instructed the model to "prefer the most likely named character over 'Unknown'", which produced 268 wrong names against only 4 Unknowns — lines credited to whoever was most prominent in the scene, above all the protagonist, who absorbed lines from the very characters addressing him (125 lines sat in bidirectional swap pairs, with the protagonist in 9 of the 18). The prompt now states explicitly that the nearest named character is often the addressee rather than the speaker, and directs the model to output 'Unknown' when the only evidence is proximity or importance. With uncertain lines now flagged and jumpable, an honest 'Unknown' is cheap to fix while a confident wrong name goes unnoticed. - **Auto-Repair's Final Sweep Never Finished Either.** Auto-repair runs unattended after every cast, and its last step used the same brute-force `includeNarrator` sweep — so the fix to the manual "Verify all characters" button did not cover it. Confirmed live: it sat for roughly 4 hours making almost no LLM calls, changed not a single segment, and ended only when the host process hit its 6-hour cap. It now uses the bounded suspect-scored pass like the rounds before it; no code path auto-triggers the unbounded sweep any more. - **"Verify All Characters" Never Finished.** The pass queued *every* narration segment — ~1000 lines on a real novel, about 190 sequential LLM calls — and in practice never completed, sitting at 0/1141 for hours before being abandoned, so it repaired nothing at all. It now uses the existing suspect-narration scoring to target ~265 plausible lines (~65 calls), which does complete. The scorer was also improved on two generic German signals: sustained first/second-person pronouns (a long speech that lost its quote marks is not third-person narration) and imperatives preceded by a discourse particle ("Nun schafft …"). Together this raises its catch rate on real "dialogue swallowed into narration" errors from 27/51 to 37/51. - **Duplicated Quotes and Speech Tags in Casting Output.** The LLM commonly returns a quote as its own segment *and* the full original sentence (quote + speech tag) as the next one. The cleanup only dropped an overlap when the narration **ended with** the dialogue text, never when it **started with** it — so the same words were spoken twice and the leftover got re-split downstream into a third stray fragment (confirmed live: `»Wir werden sehen«` appearing as two dialogue lines plus a bare `, murmelte der Ork.`; narration paragraphs restated verbatim before continuing). Duplicated prefixes are now stripped, comparing only significant characters so differing quote marks/whitespace don't hide the repeat, and the remaining speech tag is correctly typed as Narrator rather than left on the character who just spoke. - **Re-split Pass Bypassed the Fragment Check.** The narration re-split that runs after per-segment cleanup creates brand-new dialogue segments which never went through the one-word-fragment plausibility check — which is why a `»quoted title«` still survived as dialogue despite the previous fix. The check now also runs on that pass's output. - **Base Casting Also Over-Fragmented Guillemet Titles.** The 1.22.2 fix for this only covered the verification pass (`audiobookRecastUnknown`) — the initial "Cast as Audiobook" pass has its own separate per-chunk cleanup logic with the same gap, plus an existing quote-hallucination check that only looks at text over 10 characters, so a short »quoted title/name« (which does have real quote marks around it, just not as speech) sailed straight through. Confirmed live: a fresh cast run produced 5296 segments (vs. ~2100-2300 expected) with the same "»Lilo« and »Zerwas«" byline split into three one-word segments. The shared plausibility check now runs in both the initial cast and the verification pass. - **"Verify All Characters" Over-Fragmentation.** The 2nd Quality Run / "Verify all characters" pass accepted any multi-segment split the LLM returned for an existing line as long as nothing was left "Unknown" — with no check that the split was actually plausible. German uses guillemets (»«) for quoted titles/names as well as speech, so a stylistic line like a dedication byline ("»Lilo« and »Zerwas«") could get cut into three pieces with the two bare one-word "dialogue" fragments assigned guessed speakers, ballooning the cast (confirmed live: 2471 → 4728 segments, +91%, in a single verification pass). A dialogue split-piece that's a single word with no sentence-ending punctuation — never how a real spoken line reads — is now rejected before it's accepted, falling back to leaving the original line as-is. - **Character Profile Cross-Contamination.** The character-sheet extraction prompt asks for one JSON sheet per character mentioned in a passage, but never told the LLM to keep facts scoped to the specific character they describe — a scene where one character was called "leader of the Orks" could leak that title/role onto other characters just mentioned nearby, and since the (template-based) portrait prompt is built from those same sheet fields, contaminated sheets produced similar-looking character images too. Added an explicit strict-attribution rule to the prompt: only the character a fact specifically and explicitly describes gets it, never a co-mentioned character in the same scene/group — ambiguous ownership is left blank rather than guessed. - **German Chapter Headings Mispronounced.** "1. Kapitel" was being sent to TTS verbatim, and the bare digit-plus-period reads as a list marker to the TTS frontend's normalizer rather than a number — it went silent, leaving only "Kapitel" spoken after an odd pause. German chapter/part/book headings are now spelled out as spoken ordinals ("Erstes Kapitel", "Dritter Teil", "Zweites Buch") before synthesis, with correct grammatical gender per noun. Only the audio-bound copy of the text changes — casting, display, and export filenames are untouched. - **Dropped Text During Casting.** The LLM occasionally omitted a sentence or whole paragraph from its dialogue-attribution response for a passage — not mis-attributed, just never mentioned — and nothing re-checked the source text against what actually made it into the cast, so the audiobook would silently skip straight to the next paragraph. Each passage's source text is now checked sentence-by-sentence against the returned segments; any gap is recovered as a Narrator segment spliced back in next to its nearest surviving neighbour, and the cast summary now reports how many gaps were recovered. - **PDF Import Friction.** Uploading a PDF now extracts its text automatically instead of requiring a separate "Extract Text" click — the step was never optional, so the manual gate just added friction. The "OCR headings" toggle moved out of the removed prompt banner to sit next to the file-drop zone instead. ## [1.22.1] — 2026-08-23 ### Fixed - **Audiobook Modal Bundle.** Explicitly rebuilt the production JS bundle so the modal UI changes actually surface for users instead of being suppressed by browser caching. - **Narrator TTS Delay.** Eliminated a 4-10 second playback delay when encountering narrator blocks. The pre-decoder now correctly looks ahead for all synthesized audio (including narrator blocks), rather than strictly filtering for dialogue lines, preventing synchronous mid-playback decoding on ARM CPUs. ## [1.22.0] — 2026-08-23 ### Added - **Audiobook Export Modal.** Introduced a comprehensive GUI for exporting audiobooks directly from the Rehearser. - **Granular Export Controls.** Users can now select specific chapters to export, define the output format (one MP3 per chapter vs. single book file), and configure custom server-side save locations. - **Pacing Configuration.** Added UI settings to adjust pacing with precise pause durations between paragraphs and chapters. - **Tone Warning Integration.** Warnings for out-of-bounds tone settings are now displayed within the export modal for safety before batch generation. ### Fixed - **UI Synchronization.** Fixed an issue where the Rehearser's green dots for cached audio lines would not synchronize reliably during navigation updates. - **Bundle Stability.** Addressed a script hoisting issue in the production bundle that caused silent initialization failures and a "Loading..." stall. ## [1.21.1] — 2026-08-23 ### Added - **Image Generation Scripts.** Added scratch scripts for testing image generation providers (Flux, Gemini, Google Imagen, OpenAI, Pollinations). - **Character Tooling.** Added scratch scripts for character detail fetching and batch generation. - **Inventory Markdown.** Added 2026-07-16 inventory documentation. ## [1.21.0] — 2026-08-23 ### Added - **Consistent Character Renders in ComfyUI.** Replaced the generic fallback image pipeline with a deterministic, layout-first prompting strategy. A new `/api/character-generate-consistent-images` endpoint generates matching 1:1 portrait avatars and 3:2 character reference sheets (turnarounds) for the entire cast, maintaining strict character identity via hash-based deterministic seeds. - **Period and Setting Enforcement.** Image prompts now strictly enforce an Aventurian early-Renaissance (c. 1420-1500) style, explicitly clamping down on anachronistic modern fashion, high-fantasy glowing magic, and generic medieval tropes. Roles and classes automatically assign period-accurate clothing layers and headwear (e.g., chaperons, coifs). - **Automated Audiobook Export.** Added a batch script (`scripts/export_all_audiobooks.py`) for sequential, resumable exporting of all completed audiobooks into merged per-chapter MP3s. - **Book Profile Validation.** Added a `confirmed` status field to book profiles to track data completion. - **Dependency Updates.** Added `Pillow>=10.3.0` to requirements to support the new image processing pipeline. ## [1.20.46] — 2026-08-19 ### Fixed - **Completed synthesized voice previews now release their Blob URLs.** Repeated preview playback no longer retains every generated clip in browser memory. ## [1.20.45] — 2026-08-19 ### Fixed - **Temporary upload sources are now removed after conversion.** Audio previews no longer leave the original uploaded file behind in the temporary directory alongside the registered WAV. ## [1.20.44] — 2026-08-19 ### Fixed - **Voice metadata is now written atomically.** Interrupted saves cannot leave truncated `.meta.json` files that make ratings or voice settings disappear. ## [1.20.43] — 2026-08-19 ### Added - **The web container now has configurable memory protection.** Compose defaults to an 8 GB limit with a 2 GB reservation; `APP_MEM_LIMIT` and `APP_MEM_RESERVATION` can override those values for heavier workloads. ## [1.20.42] — 2026-08-19 ### Changed - **Large character tables now use browser content virtualization.** Off-screen rows can be skipped during layout and painting while the full DOM and existing interactions remain available. ## [1.20.41] — 2026-08-19 ### Changed - **Character portraits and concept art now lazy-load.** Off-screen images no longer load all at once when large cast lists are opened. ## [1.20.40] — 2026-08-19 ### Added - **Character profiles now show the generated voice filename and a play button** when the assigned voice has a saved local audio file. Playback reuses the existing voice-file preview and does not synthesize a new sample. ## [1.20.39] — 2026-08-19 ### Changed - **Low-memory devices now receive conservative batch concurrency.** Shared frontend worker pools cap themselves at two concurrent tasks when the browser reports approximately 4 GB RAM or less, reducing memory spikes during bulk operations. ## [1.20.38] — 2026-08-19 ### Added - **Characters / Cast now has a Portainer-style column chooser.** Use **Show / Hide Columns** to control the table density; visibility preferences are saved locally per user. - **Profile completeness is positioned beside moral alignment.** The sortable `filled / 32` profile indicator is now directly before the assigned voice column. ### Fixed - **The cast table’s horizontal scrollbar could disappear** after the column layout changed. The table now preserves a wide scrollable canvas and displays a dedicated horizontal scrollbar. ## [1.20.37] — 2026-08-19 ### Added - **Character profile completeness is visible in the cast table.** The table now shows how many of the 32 profile fields are filled, with visual sparse/partial/complete states and sorting by completeness. - **ComfyUI character artwork workflows are available locally.** Profile portraits and 3:2 character concept sheets can be generated through the configured ComfyUI provider, including bulk generation from the cast table. ### Changed - **Voice-design prompts now enforce audible identity constraints.** Generated prompts preserve the book language and native accent, respect gender and apparent age, and require more distinct vocal fingerprints between characters. - **Voice ratings remain review feedback rather than casting filters.** One- to three-star voices stay eligible while their prompts and profiles are improved. ### Fixed - **Assigned voice ratings could appear to vanish in Characters / Cast.** The table now waits for the shared voice library metadata before rendering ratings. - **Character-table columns could shift out of alignment.** The Book / Script row cell now matches its header, keeping the profile-completion and export columns in the correct positions. - **WhisperX Blackwell status naming and health checks were inconsistent.** The engine and status bar now identify the service consistently, and health checks no longer send unnecessary transcription requests. ## [1.20.36] — 2026-08-15 ### Added - **Character image workflow is now directly editable.** Click a character avatar or profile image to open a large editor with local upload, online-image download, and regeneration controls. Concept art opens in a wider preview/editor with the same three actions. - **Voice review is now faster in character profiles.** A compact blue play/stop control sits beside the assigned voice, and the adjacent circular regenerate control opens the editable Voice Design prompt before creating a replacement voice. - **Character table view now mirrors the Voice Library’s review data.** Each assigned voice has its own editable star rating and note, plus a Voice Design Prompt column sourced from the voice metadata (with the character-sheet prompt as fallback). - **Book context is generated during character-sheet casting when it is missing.** Genre, setting, era and language are shared with the profile, image, concept-art and voice-prompt workflows. - **Selective character casting has a larger, readable chooser.** The action is now labelled **“Select characters to cast or recast roles”**, and its list keeps names and line counts aligned. ### Changed - **Voice design prompts now use audible identity constraints.** They are concise English descriptions capped at 480 characters for CosyVoice compatibility, with explicit gender, age, native-language delivery, pitch/timbre/pace and distinctive vocal identity. Species and roles are translated into practical sound cues (for example Orc, vampire, elf, warrior, blacksmith and mage) rather than passed as lore labels alone. - **Concept-art prompt construction better anchors profile facts.** Gender, species, physical features, clothing, signature props and the book context are carried into a consistent front/side/back character-reference prompt. - **ElevenLabs voice imports remain on Get a Voice Online after importing**, enabling several voices to be collected in succession rather than forcing a jump back to the voice library. ### Fixed - **Studio casting controls could disappear completely.** The Studio toolbar used to hide its own controls after borrowing the casting footer; an asynchronous footer rebuild could then leave an empty bar. The real Identify / Cast Characters / Cast submenu bar is now relocated only after it has fully rendered, while the simple fallback controls remain visible until then. - **Selective casting could generate the wrong character.** Alias matching could resolve the selected roster name to another character’s profile. The selected sidebar name is now authoritative when a profile is generated and saved. - **Editing a Voice Design prompt could leave the character detail page blank.** The detail page now re-renders from the saved character record after generation instead of rebuilding from stale in-memory data. - **Google image generation reports provider failures instead of silently falling back**, and character-image generation skips sparse profiles rather than producing generic placeholder portraits. ## [1.20.8] — 2026-08-12 ### Added - **Fish Audio inline emotion-tag reference in Try It Out.** Selecting a Fish backend reveals a panel with all 49 documented tags (24 basic + 25 advanced); click any tag to insert it at the cursor, plus an "Insert example" button that loads a multi-emotion sample. Shown only for Fish, since it is the only engine that reads bracket tags from the text itself — other engines would speak "[happy]" aloud as literal words. ### Fixed - **Fish-Speech showed "Fetched 0 voices" and was unusable from the UI.** It clones from the app's own saved WAV library, but was missing from the set of backends populated with those voices, so the code probed the Fish server for a voice-listing endpoint it does not have and got nothing. Now reports the full library (218 voices). - **Renamed the backend to "Fish Audio S2 Pro (Clone)"** — verified the running checkpoint is `fishaudio/s2-pro` at revision `1de9996b`, matching upstream HuggingFace exactly. - **Corrected Fish's advertised capabilities.** It was listed as `style_aware: true` with "Strong: emotion/tone markers … are honoured per request" — measured to be wrong while cloning. With a reference clip, S2-Pro prepends it as an in-context example and imitates its prosody, which overwhelms inline tags: identical text showed a 10x loudness spread across emotions with no reference, but only 1.2x with one ([screaming] vs [whisper] was inaudible). Every lever was tested — canonical tags, free-form descriptions, repeated and mid-sentence tags, temperature to 1.0, truncated references, `reference_id` vs inline. The backend is now billed on what it does well: deterministic, byte-reproducible cloning. ## [1.20.6] — 2026-08-11 ### Fixed - **Critical: the whole JS bundle stopped initializing partway through**, silently disabling every module loaded after `tts-preview.js` (Rehearser, Reader, Audiobook, character sheets, …). Introduced in 1.20.5 by the new emotion quick-pickers, which guarded with `typeof REH_EMOTIONS === 'undefined'` — but `REH_EMOTIONS` is a `const` declared later in the bundle's single shared scope, and `typeof` on a `const` in its temporal dead zone **throws** rather than returning `"undefined"`. The pickers now read `window.REH_EMOTIONS` and initialize on a deferred macrotask, after the bundle has fully executed. This also invalidated earlier emotion A/B testing, which had been measuring a half-initialized app. - **Emotion instructions are now always written in English**, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest). - **Fish-Speech generation parameters (`temperature` / `top_p` / `repetition_penalty`) were never forwarded.** Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared `_apply_tts_extra_params` helper. ## [1.20.35] — 2026-08-12 ### Fixed - **Phantom characters invented from German noun phrases.** The caster emitted fragments of noun phrases as speakers — "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 much 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. ### Validation Whole-book re-casts of all three volumes, compared against their existing casts with `scripts/compare_casts.py`: | Book | Fresh cast | After auto-repair | Existing cast | Phantoms | |------|-----------|-------------------|---------------|----------| | 01 | 286 unknown | 34 | 11 (hand-corrected) | none | | 02 | 126 unknown | 7 | 0 (hand-corrected) | 5, then fixed | | 03 | 265 unknown | **9** | 25 | none | Book 03 finished below the cast it replaced, and its fused multi-turn segments — one voice reading both sides of an exchange — dropped from 60 to 6. Speaker counts stayed level with the manual casts rather than inflating, confirming the phantom filter and the label consolidation. Agreement with the hand-corrected books is 81–82%; the largest remaining error is systematic ping-pong inversion, where a whole exchange alternates onto the wrong speaker, plus epithets ("Amazone" → Lysandra) that share no tokens with the canonical name. ## [1.20.34] — 2026-08-12 ### Fixed - **Repair passes could re-introduce the speaker label variants the cast had just consolidated.** Consolidation ran only when casting finished, but every recast/repair pass assigns speakers of its own — confirmed live on a freshly cast book, which gained "Alrik" alongside "Alrik von Blautann" during auto-repair and would have split one character across two voices again. Consolidation now runs after each recast pass as well, and reports what it merged. ## [1.20.33] — 2026-08-12 ### Added - **Speaker label variants are consolidated after casting.** The caster refers to one character by several labels across a book — a bare name and a full one ("Sharraz" / "Sharraz Garthai"), a title alone ("Baronin" / "Baronin Ira von Seewiesen"), or a stray fragment ("Von" / "Oberst Alrik von Blautann"). Each variant became its own cast entry and therefore got its own voice, so a character audibly changed voice mid-scene — measured against a hand-corrected book, this accounted for more of the disagreements than genuine misattributions did. Variants are now folded together 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. ### Fixed - Quotation-mark toggle moved into the A− / A+ toolbar row, where it belongs. ## [1.20.31] — 2026-08-12 ### Changed - **The quotation-mark toggle is now pure CSS.** Guillemets are wrapped in their own `.ab-q` element at render time, so switching them off is a single class on `<body>` rather than rewriting every line's text and redrawing the whole feed — instant even with a couple of thousand segments, and the stored text is never touched. Also fixes the toggle appearing to do nothing on a freshly cast book: it previously only *added* missing marks and never hid the ones a book already had. The button moved to the bottom-right as a larger labelled pill, clear of the search controls it was overlapping. ## [1.20.29] — 2026-08-12 ### Added - **Quotation marks in the casting script, with a toggle.** German prose never quotes narration — only characters speak in `» «` — so showing the marks makes a mislabeled line obvious at a glance. PDF extraction strips them from roughly 80% of segments, so they are now *rendered* rather than stored: dialogue rows are displayed wrapped in `» «` (lines that kept their own marks are left untouched), while the saved text stays exactly as cast so synthesis and exports are unaffected. The quote button in the casting view toggles them on or off, and the choice is remembered. ## [1.20.27] — 2026-08-12 ### Fixed - **Two different characters' lines were being permanently fused into one segment.** Adjacent segments are merged when they share a speaker, but an unattributed line carries the speaker "Unknown" — so two consecutive Unknown lines compared as "the same speaker" and were joined into a single block. 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 a single voice would have read both sides of the exchange. "Unknown" means the speaker is *not known*, never that two lines share one, so Unknown dialogue lines are no longer merged. Narration and lines with a real speaker name merge exactly as before. ## [1.20.26] — 2026-08-12 ### Fixed - **Segments fusing two speakers' turns are no longer auto-attributed to one of them.** A trailing inquit only names the *last* speaker, so applying it to a block containing several turns ("…ausliefern und …" + "Er hat es nicht so gemeint" + ", unterbrach die blonde Perdia Lysandra.") silently mis-credited the earlier turn — worse than leaving it Unknown, because a wrong attribution is invisible while an Unknown is not. Blocks holding multiple turns are now skipped by the grammar rules and left for the LLM pass or manual splitting. ## [1.20.25] — 2026-08-12 ### Fixed - **A genitive name attached to "Stimme" after a quote now names the speaker** ("Darrags Stimme klang tonlos." → Darrag). The existing voice-announcement rule only inspected the narration *before* a line, so this common mirror image was left Unknown. ## [1.20.24] — 2026-08-12 ### Fixed - **Many German inquit verbs were missing from speaker attribution**, so ordinary lines stayed Unknown: `grunzte`, `versetzte`, `unterbrach`, `drängte`, `herrschte`, `keuchte`, `hauchte`, `stöhnte`, `spottete`, `höhnte`, `erklärte`, `widersprach`, `warf ein`, `setzte hinzu` and others. Also adds the thought verbs `dachte` / `überlegte` / `sinnierte`, since German inner monologue is punctuated like speech and belongs to the character thinking it. - **Trailing inquits naming two people picked the wrong one.** ", unterbrach die blonde Perdia Lysandra." assigned the line to whoever matched last; the grammatical subject is the speaker, so an optional article and adjectives are now skipped and the first name after the verb wins (→ Perdia, not Lysandra). - **A quote stayed Unknown when its own inquit was mistyped as dialogue.** A segment opening with a lower-case speech verb ("grunzte der Ork und hob den Dolch.") is an inquit by construction, whatever type the caster gave it. Those are now recognised regardless of type, and the inquit segment is corrected back to narration. ## [1.20.22] — 2026-08-12 ### Fixed - **A stale browser draft could silently destroy newer casting work.** On opening a book the localStorage draft always won and was then pushed to the server as a "backfill", so a tab holding an older copy overwrote whatever was stored server-side — confirmed live: a cast improved from 79 Unknown lines to 25 was wiped simply by opening the book in a tab that still held the old draft. The two copies are now compared by their save timestamp, the newest wins (and is cached locally), and the server is only backfilled when the local copy is genuinely the newer one. ## [1.20.21] — 2026-08-12 ### Fixed - **Split quotes with a pronoun inquit stayed Unknown.** German dialogue routinely splits as `»Ja, ungewöhnlich«` + `", antwortete er knapp."`, but every attribution rule required a literal *name* after the speech verb, so this whole class was never resolved. The antecedent is unambiguous, so it now resolves like the colon rule — to the last character named in the narration immediately before the quote. Verified against a real novel: resolves the reported case (→ Alrik) plus another, with no LLM call. - **The "Repair mislabeled dialogue" pass used the wrong prompt unless launched from the menu.** Called from Auto-repair or programmatically it fell back to the general casting prompt rather than the verification prompt, which is the one written for "is this narration actually speech?". It now defaults to the verification prompt in every entry path. - **Sentences split apart by quoted terms are rejoined.** German uses `» «` for titles and terms as well as speech ("…um »den gewundenen Weg der Krieger und Häuptlinge« handelte…"), and the casting model read those as a dialogue boundary, cutting the sentence into three fragments that no attribution pass could repair. Consecutive narration segments are now rejoined when the break is grammatically impossible, with a guard so genuine split quotes are not fused into narration. ## [1.20.18] — 2026-08-12 ### Fixed - **Toolbar menus opened upwards and were clipped off the top of the screen in Studio.** The Identify / Cast / Cast-view flyouts were hard-anchored to open above their button, which was right when those buttons lived in the casting panel's footer at the bottom of the page — but in Studio the same toolbar sits near the top, so the menu ran off-screen and its first entries were unreachable (confirmed live: "Auto-repair cast" and "Identify all characters" were both hidden). The menu now opens downwards whenever there is room, falls back to upwards when there isn't, is clamped inside the viewport, and scrolls if it is ever taller than the screen. - Identify Characters menu order now matches the order the passes should be run in, with the one-click **Auto-repair cast** first. ## [1.20.16] — 2026-08-12 ### Added - **"Repair mislabeled dialogue"** — a new casting pass for spoken lines that PDF extraction typed as narration (usually because the `» «` quote marks were lost). These were invisible to "Identify unknown characters", which only scans `type === 'dialogue'`, so re-running it could never fix them; the only pass that could ("Verify all characters") re-sent every narration segment to the LLM. The new pass scores narration for direct-speech signals (a bare inquit in the next segment, stray quote marks, imperative opening, 1st/2nd person, ending in `!`/`?`) and re-checks only the suspicious ones — measured on a real novel: **36 candidates instead of 1036, 29x fewer LLM calls**. - **"Auto-repair cast"** — chains the repair sequence automatically (repair mislabeled → resolve unknown, repeated while it still helps, then a full verification only if still above target). Runs on its own after a completed cast when more than 10 Unknown speakers remain; never after a stopped run. Disable via `localStorage['ab-auto-repair-enabled'] = '0'`. - **German speaker-attribution rules 13–19** in both the casting and verification prompts: listener/vocative exclusion (a name beside a quote is often the listener, not the speaker), split quotes with pronoun inquits, ranked disambiguation among multiple candidates, continuation over ping-pong, unnamed actors as speakers, epithet resolution ("die Elfe" → Nyrilla), and splitting segments that fuse narration with dialogue or two speakers' turns. ### Fixed - **A stopped casting run could silently leave the cast worse than before.** The automatic rollback that undoes a pass which increases Unknown speakers was gated behind `!_audiobook.cancel`, so stopping a run skipped it entirely — confirmed live: an interrupted pass took a book from 80 Unknown lines to 159 with no warning and no way back. Rollback now applies to interrupted runs too. - **Corrected a misleading attribution rule.** Rule 4 told the model that whoever acts before *or after* a quote is usually the speaker, but in German prose the neighbouring name is frequently the listener ("Marcian spürte ihre Tränen", "Marcian hörte darüber hinweg") or the person being addressed. It now applies only to active verbs, with perception/reaction verbs marking the listener. - **"Run until < N unknown" re-sent every narration segment to the LLM on every iteration** (`includeNarrator`), making each round enormously expensive and re-litigating lines that were already correct. It now uses the targeted repair pass. - Identify Characters menu reordered to the order the passes should actually be run in: identify all → repair mislabeled → identify unknown → run until threshold → verify all → check voice consistency. ## [1.20.5] — 2026-08-11 ### Added - **Emotion quick-pick added to Read Aloud, Try a Voice, and Conversation.** All three previously had either no style control at all (Conversation) or a free-text field that Fish-Speech silently ignores (Try a Voice, Read Aloud — it only reacts to an inline `[tag]` in the text). The new picker is backend-aware: on Fish-Speech it's applied as an inline tag on the text itself; on style-aware backends (VoiceDesign, CustomVoice) it fills the existing instruct field as before. - **"Run until < N unknown…"** under Read Aloud's Identify Characters menu — repeats the recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a chosen target, or two passes in a row make no further progress (e.g. GPU/LLM contention). Previously only doable by calling `audiobookRecastUnknown()` directly from the browser console. ### Fixed - **Fish-Speech's per-line emotion tags were being read out loud as literal text instead of applied as silent tone control** (confirmed live: `(excited)` was spoken as "Hexited"). Fish-Speech's S2-Pro checkpoint requires **square brackets** (`[excited]`) for a tag to be treated as a control instruction — the app was sending round parentheses. Now that the user has Fish-Speech running (`fish-speech-api`, port 38080), the backend is confirmed working end-to-end: clones the reference WAV, keeps identity stable, and now actually applies the requested tone silently. Already auto-discovered under Engines → Text to Speech → Local, no configuration needed. - **Character portrait prompts had no idea what kind of book they belonged to**, producing things like a WWI/WWII-era naval admiral, East-Asian-styled warriors, and an Asian-presenting priest in a Western medieval fantasy book (confirmed live with screenshots from a German DSA fantasy novel). The book-level context feature (`/api/book-profile` — genre/setting/era/language) already existed and was wired into voice-design prompts, but was never passed to `csBuildImagePrompt`. Also now includes the character's `race_species` field (human/elf/ork/etc., already collected by the casting LLM but previously unused downstream) in the prompt. Applies automatically to future portrait generations once a book profile is set on Read Aloud → Book Profile. - **Fish-Speech emotion tags produced no audible effect on non-English books** (confirmed live on a German audiobook: audio quality was fine, but every line was flat/emotionless). Root cause was two-fold: (1) per-line auto emotions are LLM-generated in the book's own spoken language (e.g. German "bedrohlich"), but Fish-Speech's docs require **English** tags "regardless of the spoken language" — the untranslated German word was silently ignored; (2) a **double-tagging bug**: the client already embeds `[tag]` directly into the text sent to Fish-Speech, but the server was *also* independently deriving and prepending its own tag from the separately-sent Qwen3-TTS-style instruct sentence, producing things like `[Sprich in einem bedrohlich Tonfall.] [bedrohlich] …` — neither bracket was valid English, so both were ignored. Fixed by (a) adding a German→English emotion-word translation table used when building the client-side Fish tag, and (b) making the server skip its own tag derivation whenever the text already carries one, while still falling back to the original short-text-verbatim behavior for freely-typed style instructions (Read Aloud / Try a Voice) that don't match either template. Qwen3-TTS backends are unaffected — they still receive the native-language instruct sentence unchanged, which is what they're designed to understand. ## [1.20.4] — 2026-08-10 ### Fixed - **"Casting audiobook" and "Casting unknown" could finish all their LLM work and then get permanently stuck showing "Stop Casting"**, with no error and no way to proceed, even though the underlying work had genuinely completed (confirmed live: GPU load back to idle, but the panel never left its "running" state). Root cause: in both `audiobookCast()` and `audiobookRecastUnknown()`, the post-processing that runs after the main loop finishes (deduping, rollback checks, saving the draft) was completely unguarded — any exception there meant execution never reached `view.complete()`, the only thing that actually resets the button and shows a result. Both now wrap that tail in their own try/catch, so a failure there still reaches a terminal state with a visible error instead of hanging forever. Note: this fix only applies to casting runs started after upgrading — a session already stuck in this state is running old code in memory and won't self-recover; reload and reopen the book, and the periodic autosave during casting means progress up to the point it finished should still be there to resume from. ## [1.20.3] — 2026-08-10 ### Fixed - **Heading OCR now actually recognizes decorative chapter-heading images that combine an icon/border graphic with the text** (e.g. a bold octagonal badge around "3.Kapitel") — previously it failed silently on every single one of these in a real test book (0 of 16 chapters recovered), either finding no text at all or confidently misreading the border as a stray character, because Tesseract's default full-page layout analysis gets confused by the graphic surrounding the actual text. Root-caused by testing the real OCR engine directly against the actual failing page: cropping tightly to exclude the graphic (roughly the bottom half of the heading region, where centered chapter-title text typically sits below any icon) and telling Tesseract to expect a single line of text (page segmentation mode 7) fixed it completely — verified against all 16 chapter headings in the same book, all recovered correctly and cleanly (1.Kapitel through 16.Kapitel, no garbage, no duplicates). Falls back to the original untrimmed full-heading-region OCR for headings that are already plain text with no surrounding graphic, so this is additive, not a narrowing of what already worked. ## [1.20.1] — 2026-08-10 ### Fixed - **A PDF page that produces zero extractable text (no real text layer AND heading OCR either found nothing or failed the confidence threshold — common for a page that's entirely a decorative divider graphic, e.g. one book had a page that was just a small icon with no chapter number at all) used to be completely invisible to page tracking**, silently shifting every subsequent page number out of sync with the actual PDF for the rest of the book. Confirmed live: investigated a real book's chapter-heading pages directly — one divider page rendered as literally just a small icon graphic (no OCR-recoverable text by design, not an OCR failure), and every such page was dropping out of `readerState.sentences` entirely, taking its page number with it. Every PDF page now keeps at least a placeholder marker (empty text — never spoken, never shown as a line) so page-break reconstruction after LLM speaker-attribution never skips a page number. ## [1.20.0] — 2026-08-10 ### Added - **App Routing rows now have a playback-speed multiplier (0.5x-2x) and a preview button.** The speed is applied server-side to real routed requests via a pitch-preserving ffmpeg tempo change (not a naive frame-rate shift, which would make a sped-up voice sound like a chipmunk) — see `core/audio.py:_change_tempo`. The preview button synthesizes the row's exact output voice directly (no need to save the route first) and plays it back at the chosen speed for an instant "does this sound right" check, using a short phrase localized to the row's own language setting. ## [1.19.4] — 2026-08-10 ### Changed - **Audiobook export now synthesizes lines one at a time instead of 2 concurrent workers.** Every TTS backend this app talks to (Voice Clone, Voice Design, Fish-Speech, and the other local engines) is a single self-hosted GPU model instance, not a horizontally-scaled service — confirmed live, twice now, with two different backends: 2 concurrent requests reliably push at least one past the reverse proxy's 60-second timeout under real load, causing seemingly-random per-line failures that can doom a whole multi-hour export. Serial is slower per line but doesn't waste time on doomed, retried requests — net faster in practice, and actually finishes. ## [1.19.3] — 2026-08-10 ### Fixed - **The looser chapter-heading detection from 1.19.0 caused ordinary narration sentences to be misdetected as chapter breaks, replacing real paragraph text with a thin marker line — confirmed live as "lots of empty pages" in the A4 pagination view.** Two separate bugs: (1) requiring the keyword only at the start of the line, with nothing checked afterward, meant any short sentence starting with a common word like German "Teil" ("part") — e.g. "Teil des Grundes war unklar." ("Part of the reason was unclear.") — matched as a chapter; now whatever follows the keyword must actually look like part of a heading (empty, a bare number, or a colon/dash-separated subtitle), not a normal grammatical continuation. (2) The leading-numeral stripping regex treated a bare "C" as valid Roman numeral 100 with no requirement that anything sensible follow it — so it silently ate the "C" off the front of "Chapter", turning "Chapter 1: The Beginning" into "hapter 1: The Beginning" before the keyword check ever ran. Verified against the actual book that surfaced this (1998 lines): zero false positives, one correct real match ("14. Kapitel"). ## [1.19.2] — 2026-08-10 ### Fixed - **The tone/identity comparison table always suggested Voice Design as the alternative to Voice Clone, never Fish-Speech, even when Fish-Speech was running** — the suggestion logic just took the first backend matching one criterion (`style_aware`, or `uses_wav`) rather than preferring one matching BOTH, so it never surfaced the strictly-better option (tone-aware AND keeps voice identity) over a partial fix. Now prefers a backend satisfying both properties before falling back to a partial match. ## [1.19.1] — 2026-08-10 ### Added - **Chapter headings now render as a visible horizontal-rule marker with the chapter's own title/number in the Script Rehearser/Studio Stage view**, not just as an audible pause in the finished export. Reuses the exact same detection `audiobookExport()` uses for chapter/file boundaries, so the fix to that detection (numbered OCR headings like "1.Kapitel") shows up here too — scanning through a long script now makes chapter breaks visually obvious instead of looking like one continuous, undifferentiated wall of narration. ## [1.19.0] — 2026-08-10 ### Added - **Audiobook exports now have real pauses between paragraphs and chapters, plus an optional custom chapter-transition sound** — `mergeWavBlobs()` used to concatenate every line's clip with literally zero gap, which read as characters teleporting mid-scene with no beat between paragraphs, let alone chapters. New "Pacing" section in Studio → Perform & Export lets you set the paragraph pause (default 2s) and chapter pause (default 4s) independently, and upload a short sound (chime, page-turn, etc., max 2 MB) to play before each chapter's pause — decoded and resampled client-side to exactly match the narration's own sample rate so it splices in cleanly rather than corrupting the merge. Settings persist per-browser via localStorage. ### Fixed - **Chapter headings recovered via OCR (from PDFs where the heading is baked into the page as an image) usually weren't recognised as chapters at all**, because `audiobookIsChapter()` required the keyword ("Kapitel"/"Chapter"/etc.) to be the very first word — but OCR'd numbered headings commonly read "1.Kapitel" or "I. Kapitel" with the number first. This silently defeated the documented "one file per chapter" audiobook export for any book using numbered image headings (confirmed live: a full novel exported as a single 7.5-hour file instead of per-chapter files). The chapter regex now tolerates an optional leading number or roman numeral before the keyword. ## [1.18.23] — 2026-08-09 ### Changed - Moved the backend tone/identity comparison table into the "Generate full audiobook" toggle row's spare width, instead of rendering as its own full-width banner below the toolbar. ## [1.18.22] — 2026-08-09 ### Changed - Redesigned the backend tone/identity warning as a proper comparison table (current backend vs. the suggested alternative, "Tone control" and "Voice stays identical" as columns with ✓/✗) instead of a run-on sentence with a "Switch" button awkwardly wedged into the middle of it. ## [1.18.21] — 2026-08-09 ### Changed - "Skip narrator" now defaults to unchecked (narration reads by default) and moved to the front of the Stage toolbar. - The backend tone-support warning ("Switch to X for reliable tone…") now has an actual "Switch to X" button instead of just naming the better backend in a sentence and leaving you to go find it yourself in a settings dropdown. ## [1.18.20] — 2026-08-09 ### Fixed - **A single transient synthesis failure could silently doom an entire multi-hour audiobook export.** `fetchTtsPreviewBlob`'s own retry logic only covers connection-level failures — `fetch()` doesn't throw on a non-2xx HTTP response, so a backend hiccup (confirmed live: transient 500s clustered in the first ~50 lines, most likely GPU/engine warm-up contention from the two parallel export workers both starting cold) skipped that retry layer entirely and permanently failed the line. Different, completely ordinary lines failed across repeated attempts — never the same one twice — confirming this was never about any specific line's content. The export now retries a failed line up to 3 times with backoff before giving up on it for real. ## [1.18.19] — 2026-08-09 ### Fixed - **Reverted the sticky "Generate full audiobook" toggle from 1.18.18 — it broke scrolling entirely.** Stacking two sticky headers (the toggle plus the already-sticky, now-wrapping multi-row transport bar below it) could exceed the viewport height on a real window, leaving nothing scrollable visible at all. The toggle now scrolls away normally again, same as before 1.18.18; only the transport bar stays sticky. Will revisit with the wrapped toolbar's actual height accounted for, tested in isolation before shipping again. ## [1.18.18] — 2026-07-30 ### Fixed - **The font-size (-A/+A) buttons were ~18px shorter than every other toolbar button** (`.reh-fontsize-btn` overrode padding to 2px 7px against the base button's 9px 18px). Removing the override alone left a smaller residual gap (plain "A" text computes a shorter natural line-height than icon+text buttons) — matched the sibling buttons' actual rendered height directly instead of guessing at line-height multipliers. Verified live: exact match. - **Studio's "Generate full audiobook" toggle wasn't sticky, so it scrolled out of reach while reading through a long script** — only the transport bar below it was. Both now stick together, stacked in the correct order, so the mode toggle and play controls stay visible throughout. ## [1.18.15] — 2026-07-30 ### Changed - Renamed the Stage toolbar's "Skip desc." checkbox to "Skip narrator" — clearer about what it actually controls (whether the Narrator voice reads scene/action text during playback) now that the toggle genuinely works. ## [1.18.14] — 2026-07-30 ### Fixed - **The Stage toolbar's font-size, page-mode, Train, and Exit buttons were unreachable on any normal (non-ultrawide) window.** `.reh-console-actions` had `flex-shrink: 0`, and flex items default to `min-width: auto` regardless of any width set — together these meant the block could never shrink below its full unwrapped width (~1560px), so the browser's wrapping algorithm never considered it "too wide to fit" and it just overflowed silently off the right edge with no scrollbar to reach it. Confirmed live at a realistic 800px window width. Now shrinks and wraps onto additional rows instead. ## [1.18.13] — 2026-07-30 ### Fixed - **Narration playback ignored the "Skip descriptions" toggle (and Studio's "Rehearse ⇄ Audiobook" toggle, which just flips the same flag) once any narrator voice was assigned.** The batch pre-skip optimization correctly checked both "skip mode is on" and "no narrator voice set," but the actual per-line narration-speak branch below it never re-checked the skip flag at all — so as soon as a book had a narrator voice configured (the normal case for any book actually being produced), narration played regardless of the toggle's position. This is why the toggle looked like dead weight: it couldn't turn narration OFF, only ever left it stuck ON. Both branches now consistently respect the toggle. - Studio's audiobook-mode toggle now warns clearly if you turn it on with no narrator voice assigned yet, instead of silently doing nothing. - Root-caused "emotions still not recognisable" on Book 02's audiobook: its entire line-audio cache (1761 cached clips) was synthesized *before* the per-line emotion-instruct engine fix was actually deployed to the running container — confirmed by comparing file timestamps against the fix's deploy time. Since the cache key is a hash of (text + voice + instruct) and none of those changed, every future playback kept serving the identical pre-fix, flat-delivery audio forever, with the engine fix having no way to ever take effect. Cleared the stale cache; the next playback or export for Book 02 will synthesize fresh against the corrected engine. Book 01's cache was unaffected — it was built entirely after the fix. ### Fixed - **A voice that started as a Voice Design creation was routed to the Voice Design engine forever, even after it had a proper reference clip saved.** Voice Design has no seed parameter at all, so every playback of a designed voice was an unpinned, unreproducible roll regardless of any seed pinned for it. Routing now checks only whether a reference clip exists (`has_ref`) — the actual reason Voice Design is needed at all — not voice origin. A designed voice with a saved reference now clones like any other voice, which is what makes a pinned seed actually take effect for it. Applies to line playback throughout the app (Rehearsal, audiobook export, Try It Out) and to the Seed Finder's own backend default. ## [1.18.11] — 2026-07-29 ### Fixed - **Designed voices now store their full design prompt.** For a designed voice the prompt IS the voice's identity — the engine reproduces it from that text alone — but the only copy saved was the `note` display summary, which is deliberately clipped to 240 characters (43 of 73 voices were cut off mid-sentence). The complete prompt is now saved in its own field, so the TTS engine can register the voice from the whole description rather than a truncated one. Existing voices keep working from the clipped copy — it still carries gender, accent and timbre — and pick up the full text the next time they are redesigned. ### Note - This release pairs with an engine-side fix (in the `tts-dgx-spark-faster-qwen3-tts` repo) for two bugs that made custom voices unusable: stale speaker embeddings causing cloned voices to ignore the requested text entirely, and designed voices never being registered with the Voice Design engine, which silently substituted a bundled British preset. See that repo's history for details. ## [1.18.10] — 2026-07-29 ### Fixed - **A voice's pinned seed (set via the Seed Finder / Batch Seeds tools) was saved but never actually used — for two independent reasons.** (1) The seed gets written to the voice's own metadata, but every normal generation call (Try It Out, Rehearsal, audiobook export) built its request without ever reading that value back — only the Seed Finder's own one-off benchmarking codepath looked at it. (2) Separately, the internal "voice_design_playback" stability profile (added in 1.18.4 specifically so ongoing dialogue wouldn't hardcode `seed: 0` over every per-voice pin) wasn't recognized by the backend-name normalizer, which silently coerced it back to "voice_clone" — reintroducing exactly the hardcoded `seed: 0` it was designed to avoid. Together these meant a designed voice could still sound noticeably different (including gender-adjacent timbre drift) from one generation to the next even after "pinning" a seed for it — the pin was completely inert. Confirmed live post-fix: a Voice Design request for a voice with a saved seed now actually carries that seed. Every voice_clone/voice_design/customvoice/streaming request falls back to the voice's own saved seed when the request doesn't already specify one explicitly. - **"Apply character persona" silently did nothing when the selected voice had no persona text saved** (persona is a field you type manually on the Voice Inspector page — it's never auto-filled from a character sheet) — indistinguishable from the feature being broken. The checkbox is now disabled with an explanatory tooltip when the voice has no persona, and the backend returns a clear error instead of a silent no-op if it's checked anyway. - **Chunked preview generation (auto-enabled for text over 200 characters) dropped "Apply character persona" entirely**, regardless of whether the checkbox was checked — it was hardcoded off for that code path only. ## [1.18.9] — 2026-07-29 ### Fixed - **Highpass/lowpass filters (the new Telephone/Radio bandpass control from 1.18.7) were far too weak to be audible** — confirmed via spectral analysis: pedalboard's filters are single-pole (~6 dB/octave), so the residual harmonic distortion left over from compression (see below) still poked straight through a single pass, leaving the "outside the passband" energy roughly unchanged or even higher than the original. Each highpass/lowpass now cascades 3 internal stages (~18 dB/octave) — confirmed live: content below/above the cutoff is now reduced by 7-14 dB instead of being essentially untouched. ## [1.18.8] — 2026-07-29 ### Fixed - **The compressor's makeup gain (added in 1.18.5) was clipping.** Confirmed live via spectral analysis: makeup gain routinely pushed peaks to ~1.9 (well past the ±1.0 digital ceiling), and the final hard clip in the effects pipeline sliced that overshoot off — producing broadband harmonic distortion that swamped the actual intended effect. This is why the Telephone/Radio presets still sounded like "no difference": the distortion, not the intended band-limiting, was dominating the output. Replaced the hard clip with a proper limiter after makeup gain, so peaks are caught smoothly instead of sliced. ## [1.18.7] — 2026-07-29 ### Added - **Telephone/Radio effect presets now actually band-limit the audio** — a new "Bandpass (telephone/radio)" control (high-pass + low-pass cutoff sliders) was added to the Try It Out effects panel and wired into both presets. Compression alone (even correctly calibrated) doesn't sound like "a phone call" — the defining trait is narrow frequency range, which the presets never applied and the UI never exposed. ## [1.18.6] — 2026-07-29 ### Fixed - **The last fix for "Try It Out" showing only generic Voice Design presets instead of your own voices made things worse — it went from "wrong voices" to "no voices found."** Voice Design's raw discovery endpoint only ever lists its own bundled presets, never any custom voice — so filtering that list against the user's own library (the same technique that correctly works for Voice Clone/Streaming) always produced an empty intersection. Fixed properly: for Voice Design, the dropdown is populated directly from the user's own active voice library instead of trying to filter the engine's useless discovery list at all. ## [1.18.5] — 2026-07-29 ### Fixed - **Browser Back/Forward didn't work inside the app at all** — every section change called `history.replaceState`, which overwrites the SAME single history entry instead of adding a new one, so the browser had nothing from the app's own navigation to step through. Clicking Back skipped straight past the whole app to whatever page was open before it. Switched to `pushState`; the existing hashchange listener already handled Back/Forward correctly, it just never had real history entries to respond to. - **"Try It Out"'s Voice Design dropdown only ever showed the engine's own built-in presets** (vd_british_male, vd_german_male, ...) — never any of your own designed voices, even after designing 70+ characters for a book. Now filtered to your own voice library, same as Voice Clone/Streaming already were. - **Audio effects had no audible impact** — root-caused live: every effects preset's compressor threshold sat ABOVE where our own TTS output actually lives (all voices are normalized to ~-20dBFS; presets ranged from -10dB to -18dB), so a compressor threshold-gated to louder signal had almost nothing to act on — confirmed the "Telephone" preset changed a real speech clip's loudness by under 0.1%. Lowered every preset's threshold to actually engage with real output, and added makeup gain after compression (a plain compressor with no makeup gain only ever shaves peaks quieter — it never produces the louder, "punchier" sound people associate with compression, which is why even a correctly-engaging compressor was hard to notice). ## [1.18.4] — 2026-07-29 ### Fixed - **Several characters were split across multiple speaker labels with inconsistent — sometimes wrongly-gendered — voices**: the underlying book text refers to the same person by different names/titles/epithets at different points (a vampire lord called "Roger," "Zerwas," and others; a character nicknamed "Irre" who is really "Uriens"; a guard referred to once as "Wächter" and once as "Wächter des Turmes"), and each distinct label had gotten its own independent cast entry and voice. Confirmed live: merged 8 such groups onto one consistent voice each. Four of them ("Inquisitor," "Jägerin," "Linosch," "Sharraz" — 61 lines total) had NO voice assigned at all, meaning that dialogue was being silently dropped from the audiobook entirely, not just mis-voiced. - **Emotion stopped coming through audibly right after the accent-reinforcement fix landed** — root cause: Qwen3-TTS's own prompting guidance warns it doesn't reliably follow multiple conflicting instructions in one prompt, favoring one over another. The accent clause was being placed FIRST in every line's instruct, making it the most prominent instruction on every single line and likely crowding out the (shorter) emotion tag that followed it. Reordered so emotion leads and the accent reminder trails, instead of the other way around. ## [1.18.3] — 2026-07-28 ### Fixed - **The anti-American-accent instruction only ever applied to the one-time call that designs a new voice — never to any of the actual lines it goes on to read.** Root cause: the accent clause was built fresh at design time and layered on top of the character's saved voice-quality description, but never written back into that saved description — so every ONGOING line's instruct (built from the saved profile) carried zero accent guidance. Since each synthesis call is stateless, the model has no memory of the original design call's instructions; omitting the clause here meant real narration got none of it at all, only the initial creation did. The same accent clause now gets added to every line's instruct, not just the design call — Stage playback, Synth all, and the audiobook export all benefit automatically since they share the one `_buildInstruct` function. ## [1.18.2] — 2026-07-28 ### Fixed - **Seed Finder's default test sentence for German (and every other) voices was itself a mid-sentence mix of German and English** — confirmed as the actual cause of "the seeds are horrible, that's a mixture of English and German": the `DE`, `EN`, and "mixed torture-test" constants were all literally the same string, including a full English sentence ("The system administrator successfully configured the customized Docker stacks...") baked into the "German" default. Replaced with genuinely single-language defaults per voice, and — better — the voice's own saved reference transcript (real book content) is now used first when available, since that's exactly what the voice will actually read. - **Designed-voice playback never told the engine what language to expect, unlike every other backend** — left at "Auto" (auto-detect) for every single line, which is least reliable on short dialogue lines. Now passes the voice's own language explicitly, same as Voice Clone/Streaming/CustomVoice already do. ### Investigated - Tested whether an already-designed voice could be served through the Voice Clone engine instead (the officially-documented path for consistent multi-line reuse) — the clone engine doesn't recognize a Voice-Design-only voice_id at all (confirmed live, before and after a full backend restart), so this would require a proper conversion step this app doesn't yet implement. Not pursued further this round; noted for a future pass. ## [1.18.1] — 2026-07-27 ### Fixed - **The previous loudness fix (normalizing every clip independently to one target level) had a real side effect: it erased a voice's own whisper-vs-shout dynamics along with fixing the cross-voice level gap.** Confirmed live: a whispered line came out LOUDER than its own neutral reading once both were pushed to the same target — exactly backwards. Replaced with a fixed per-voice gain offset (the voice's own already-computed reference gain from Calc dB, applied uniformly to every line from that voice) — this shifts each voice's baseline to match others without touching how loud one line is relative to another from the same voice. - Audited all 72 designed voices used in "Die Entdeckung" for the anti-American-accent instruction added earlier this session — 34 of them (nearly half) predated that fix and never got it. All 34 are being redesigned with the current prompt builder. ## [1.18.0] — 2026-07-27 ### Fixed - **Emotion tags barely registered on designed voices** — root cause: the casting pass tags emotions in the book's own language (e.g. German "fordernd", "entschlossen"), but the instruct sentence wrapping it was hardcoded English ("Speak in a fordernd manner."), dropping a German word into an English carrier sentence — a much weaker signal than a natural sentence in one language. The template now matches the target voice's own language (inferred from its `DE_`/`EN_`/... id prefix), applied everywhere an instruct gets built: Stage playback, Synth all, re-synthesis, Train mode, and — the one that actually matters for a finished audiobook — the export path itself, which had drifted out of sync with the others and was still building English-only instructs. - **The Narrator sounded noticeably louder than designed-voice characters in a finished export** — nothing in the synthesis or merge pipeline ever loudness-matched clips from different backends against each other. Every freshly-synthesized clip is now normalized to the same target used for voice reference files; a new endpoint also normalizes an already-built cache in place (pure audio processing, no resynthesis) so an existing book doesn't need a multi-hour re-synth just to fix levels. ### Added - Per-clip and whole-book-cache loudness normalization endpoints (`/api/audio/normalize-wav`, `/api/line-audio/{book}/normalize`). ## [1.17.99] — 2026-07-27 ### Fixed - **Designed voices could occasionally speak nonsense or repeat text mid-line, unlike cloned voices** — root-caused by comparing our own request payloads against the actual Qwen3-TTS API: the app already applies a stability profile (temperature/top_p) to every Voice Clone request to keep it consistent, but Voice Design's stability profile was deliberately left empty — correct for the ONE-TIME call that designs a brand-new voice (you want fresh randomness there, or every character sounds the same), but that same empty profile was also being used for every ONGOING line of dialogue read by an already-designed voice, which needs the opposite: consistency, not randomness. Split into two separate profiles — voice creation stays unconstrained, but reading a line from an existing designed voice now gets the same stability treatment as Voice Clone. Verified live: the same line synthesized 5 times in a row went from inconsistent/occasionally-wrong to a perfect transcription match every time, with emotional instructions (tested angry vs. whisper vs. neutral) still working correctly. - All 72 designed voices in "Die Entdeckung" were pinned to a specific seed via an automated hunt (tries a few seeds per voice, keeps whichever one round-trip-verifies correctly) rather than left on a random seed per call — the other major source of the "sometimes fine, sometimes not" inconsistency. - Added a matching "Voice Design (reading a line)" params field in Settings, alongside the existing "Voice Design (creating a new voice)" one, so the two are no longer silently sharing one config slot. ## [1.17.98] — 2026-07-26 ### Added - **Voice round-trip verification**: synthesizes a voice's own reference line (or a language-appropriate default), transcribes the result back with Whisper, and compares it word-for-word to the original text. A duration/wpm-only benchmark can't tell "read the line correctly" from "repeated it twice" or "said something unrelated" — both can produce a perfectly normal-looking duration and pass every prior check; this actually checks the words. New "Verify (STT)" button in the Voice Library toolbar runs it over the active/selected voices and lists exactly which ones failed and what they said instead. - The same round-trip check now runs automatically as part of every voice design/redesign attempt — a low-scoring attempt is rejected and retried just like a clipped or implausible-wpm one, so a bad design no longer needs to be caught by ear after the fact. ## [1.17.97] — 2026-07-26 ### Fixed - **A designed voice (no reference WAV) failed outright — every time, no retry possible — whenever a bulk action picked the wrong backend for it.** Root cause found in three places at once: "Precompute" and "Batch seeds" both used one globally-selected backend for every voice in a batch, and the per-voice Seed Finder didn't offer Voice Design as an option at all — so a selection that was entirely designed voices (like this book's 24-character redesign) got "Precomputed 0 embedding(s), 72 skipped/failed" and a batch-seed run that failed every single job. Every synthesis path in the app (Stage playback, Synth all, Audiobook export, Precompute, Batch seeds, Seed Finder) now resolves the correct backend per voice automatically — a designed voice always routes to Voice Design regardless of what's globally selected; every other voice still respects it. - Added a "Voice Design" option to the per-voice Seed Finder's backend selector (it only offered Voice Clone/Streaming before), auto-selected when opening a designed voice. ## [1.17.96] — 2026-07-26 ### Fixed - **A bulk synth/export job (Synth all, Audiobook) could lose lines outright to "Failed to fetch" with zero retry** — confirmed live: a ~2000-line export lost 346 lines this way. Same root cause as the earlier voice-design fix: a GPU-contended TTS backend container can be mid-restart for a handful of seconds, and a job hitting the TTS endpoint hundreds of times in a row will reliably catch that window at least once. The core `fetchTtsPreviewBlob` helper (used by every synthesis path in the app) now retries a couple of times with a short backoff on a raw connection failure before giving up — a real HTTP error response is still surfaced immediately, only a connection that never got a response at all is retried. ## [1.17.95] — 2026-07-26 ### Fixed - **Reassigning a character's voice in the Library/Studio Voices tab didn't reach an already-open Stage session for that book — the export kept quietly using the OLD voice, and its cached audio for that character's lines never got invalidated**, which is why a re-export right after changing voices could finish suspiciously fast: it wasn't skipping work because nothing needed to change, it was skipping work it should have redone. A rehearsal loads its cast once from its own saved record, and an explicit prior voice always takes precedence over a fresher one from the shared roster — by design, so casting choices aren't clobbered — but that meant a later voice change had no path back in at all. Fixed: saving a character's voice now updates a matching, currently-open rehearsal's cast immediately and marks that speaker's already-synthesized lines stale, so the next synth/export picks up the new voice instead of reusing old audio. ## [1.17.94] — 2026-07-26 ### Fixed - **Voices designed for sparse/generic cast entries ("Frau", "Mann", "Geliebten", "Kissen"...) could come out with the wrong gender and read an English placeholder sentence, even in an all-German book.** Root cause: these are minor entries the casting pass extracted from a plain noun in the text rather than a real named character, so their sheet never gets a `gender` filled in and has no descriptive text to detect a language from — the design code was defaulting straight to English + neutral gender instead of falling back to the book's own already-known language. Fixed: the reference line now falls back to the book's resolved language (same lookup already used for the voice-design instructions) instead of English, and a handful of unambiguous German nouns ("Frau", "Mann", "Junge", "Mädchen", "Herr", "Dame") get their obvious gender when the sheet's own field is empty. ## [1.17.93] — 2026-07-26 ### Changed - **The voice-preview play button on each character's row/card now plays that voice's own stored audio file instantly instead of re-synthesizing a sample through the TTS engine on every click.** My Voices already has a fast "play original recording" button for exactly this; the character-table button was doing the slow, GPU-heavy thing instead for no benefit — you're just trying to confirm which voice a character has, and the exact audio already sits on disk. ## [1.17.92] — 2026-07-26 ### Fixed - **"Voice design failed: Failed to fetch" could hit almost any character when designing several back-to-back** — root-caused live from the server's own access log: a redesign that changes a voice's reference audio schedules a TTS backend restart so the change actually takes effect, and that restart was firing immediately after every single character (each one taking ~10-30s to come back). Clicking "design" on the next character while that restart was still in flight sent its request straight into a dead backend, failing outright with no retry. Fixed two ways: (1) individual (non-bulk) redesigns now coalesce rapid back-to-back restarts into one, firing a few seconds after the last click instead of after every single one; (2) the design/save calls themselves now retry a couple of times with a short backoff on a raw connection failure before giving up, since the window is normally only a few seconds wide. ## [1.17.91] — 2026-07-26 ### Fixed - **A voice (or image prompt) saved for one character could silently land on a completely different character instead** — confirmed live for "Marcian": every attempt to design/save his voice actually overwrote "Alrik von Blautann"'s record, because Alrik's own LLM-extracted `aliases` field happened to literally list "Marcian" as one of his alternate names (an extraction slip). The save path re-derives which character record to write to by scanning all characters for a shared alias/name token — meant for deduping a freshly-extracted sheet at casting time, but reused here too, so one stray shared token silently redirected the write to the wrong character with no error. Fixed by having the voice/image-prompt save paths point at the exact character record they already have in hand (its own id) instead of re-guessing by alias every time. ## [1.17.90] — 2026-07-26 ### Fixed - **Voice design could fail outright ("produced broken audio after 3 attempts") when the real cause was an unrelated backend being briefly unreachable, not bad audio.** Root-caused live for character "Marcian": the post-design quality check benchmarks each attempt against the voice-clone TTS backend, which was stuck in a GPU-out-of-memory restart loop (fighting the LLM engine for shared GPU memory) — every benchmark call got a flat "Connection refused"/"Connection reset", which the check then treated exactly like a genuinely corrupted recording, burning all 3 attempts and leaving the character with no voice. Now a connectivity failure against the benchmark backend is treated as "couldn't verify" rather than "verified broken" — the design is accepted (a real defect still gets caught and rejected normally whenever the benchmark backend actually responds). ## [1.17.89] — 2026-07-25 ### Added - **"Design a voice" now opens an inline, editable prompt popup instead of navigating away to the Design a Voice page.** Clicking the design/redesign icon on a character (card, table row, or detail modal) shows the same saved/built voice-design prompt in a text box right there, with a single "Generate voice" button — edit the description and regenerate without ever leaving the Studio/Library screen, and without needing a way back afterward since you never left. ## [1.17.88] — 2026-07-25 ### Fixed - **The voice picker opened by the "Auswahl" button in the Voices table (and in the character detail modal) could appear far away from the button that opened it** — both passed the entire row/card or modal box as the anchor element instead of the button actually clicked, so the picker's position (measured via that element's own bounding box) landed wherever that much bigger container happened to start or end rather than next to "Auswahl". Confirmed live: fixed to anchor to the actual clicked button — the popup now opens directly below/beside it as expected. - **That same picker only ever showed the first 60 voices**, silently hiding the rest of a larger library unless you already knew to search by name — confirmed as the reported "not all the voices I have" (154 enabled voices, only 60 ever shown). Raised to 500 (a sane upper bound, not a real-world limit) so a realistic library shows in full. ## [1.17.86] — 2026-07-25 ### Added - **The Stage page now asks the server which lines already have cached audio on disk, in one batch request, and lights up their "pre-synthesized" dots accordingly.** Previously every dot only ever reflected `rehState.synthCache` — this browser tab's own memory, empty on every fresh page load — so a script that was fully "Synth all"-ed and correctly persisted to disk in an earlier session still looked completely unsynthesized after a reload, with nothing indicating the cached audio was actually right there. Confirmed live: after a reload, lines with an on-disk cache file correctly show their dot again without downloading any audio up front (that still only happens lazily, right when a line is about to play). - **"Exports" button on the Stage toolbar** — browse and re-download audiobook chapters already exported for the current book (via the server-side saving added in 1.17.85) without re-running the export. The export-results panel also gained a **"Download all (.zip)"** link when there's more than one chapter file. ## [1.17.85] — 2026-07-25 ### Added - **"Clean cache" button on the Stage toolbar** — scans the current script for every cache key its lines would actually use right now, and deletes anything else cached on disk for this book. Needed because editing a paragraph never deletes its old cached file (the write path only ever knows the new content's hash, not whatever it used to hash to before the edit) — this is the cleanup pass for that dead weight, run whenever you want rather than automatically. Verified live: cleaned up 32 orphaned files after a round of edits. - **"Audiobook" export now also saves each chapter server-side and shows a results panel with real download links**, not just a browser download that lands wherever your browser settings put it with no record in the app of where it went. The panel also names the exact chapter files so you know what actually came out of the export. ### Changed - **The Stage/Perform & Export page is noticeably wider** (794px → 1180px) — it was simulating a literal A4-page width regardless of screen size, wasting a lot of horizontal space on wide monitors compared to the Characters view's fuller-width text column. ## [1.17.84] — 2026-07-25 ### Added - **Every line synthesized via "Synth all", re-synthesizing a stale line, or just playing a line individually now persists to disk**, keyed by a hash of exactly what determines its sound (text + voice + tone), in a folder named after the book — not just kept in the browser tab's memory like before. Editing a paragraph changes its hash, so the edited version simply never matches the old cached file and gets synthesized fresh; an untouched paragraph keeps reusing its file indefinitely, even across a page reload or a totally new browser session. This is what makes playback "flawless and fast" with no GPU wait between paragraphs once a book has been synthesized at least once — confirmed live: a second play of an already-cached line hits the new `/api/line-audio` endpoint (a plain 200) instead of `/api/tts-preview` (the actual TTS call) at all. The full "Audiobook" export also checks this cache before synthesizing, on top of the in-memory reuse added in 1.17.79. ### Fixed - **"PDF pages" mode never actually produced separate pages — the whole book rendered as one continuous page**, identically to "Scroll" mode, on every book regardless of how many real page breaks its source document had. Root cause: `parseScript`'s page-break detection tested the line AFTER `.trim()` — and `.trim()` strips `\f` (form feed) as whitespace along with everything else, so the exact marker the check was looking for was always gone by the time the check ran. A real book with 232 page marks in its source text was producing zero pagebreak lines after parsing. Fixed to check the original, untrimmed line. "PDF pages" (break at the document's own real pages) is now also the default mode instead of "A4 pages" (break purely by content height, ignoring the source document's pages entirely). ## [1.17.82] — 2026-07-25 ### Changed - **Auto-designed voices now explicitly steer away from an American English accent by default.** Gender and language were already correctly sourced from the character's own sheet and the book's resolved language respectively (confirmed in the actual `/api/voice-design` call, not guessed per character) — but nothing ever told the model what accent to actually use, and its default leans American-English regardless of target language, a recurring complaint even on non-English books. The instruct text now explicitly names the accent: an authentic native accent for the book's own language when it isn't English, or a neutral British/international English accent when it is — never American. Applied both to the automated bulk "Auto-design voices" path and the LLM-authored `voice_design_prompt` field used by the manual per-character Design flow. ### Fixed - **Clicking a line's play button while a previous, not-yet-synthesized line was still loading could let that stale synthesis cut in and start playing anyway** — on top of, or right over, the line actually requested, with no clean way to stop just the stray one. The guard after awaiting a fresh TTS synthesis only checked a bare `rehState.playing` boolean, which the new click's own stop-then-start had already flipped back to `true` by the time the stale continuation resumed. Now checks that the line index itself hasn't changed since this specific playback was started, not just that *something* is playing. - **The narrator paragraph play button (added in 1.17.74) showed two overlapping play icons on one small button** — it shared a CSS class with dialogue's own avatar-badge play button, which layers a small badge icon on top via `::after`, on top of this button's own separate icon. Narrator play buttons are now styled as that same badge directly instead of stacking both. - **That narrator play button also never visually indicated a line was playing** — same static play icon the whole time, no way to tell that clicking it again would stop it. It now swaps to a stop icon while its own line is the one active. - **The Stage sidebar's character names were always shown in shouting-case** (e.g. "ALRIK VON BLAUTANN") — it was rendering the raw speaker key straight out of script parsing (which follows all-caps screenplay convention for speaker tags) verbatim, instead of the properly-cased name already sitting in the Library. Now prefers the Library's own casing, falling back to a simple per-word title-case for the rarer speaker key with no Library match, instead of shouting-case either way. The per-character colored name text (independent of this) has also been removed — names use the normal (theme) text color again, with the color cue staying on the avatar/border instead. --- ## [1.17.79] — 2026-07-25 ### Added - **Shift-click range select in the Voice Library table** — click one row's checkbox, then shift-click another, and everything in between gets selected/deselected to match, the same convention as a file manager. Checking dozens of rows one at a time for a bulk action (Delete, Set tag, Benchmark, …) was the alternative. ### Fixed - **Exporting a full audiobook re-synthesized every single line from scratch via TTS, even ones already pre-synthesized by "Synth all"** — silently redoing work that was already done, easily 20+ minutes for a real novel with no visible sign anything had gone wrong. Confirmed as the actual explanation behind "clicked Audiobook and got no file": it wasn't stuck or failing, it just had a lot of unnecessary work left to do. Already-synthesized, non-stale lines are now reused directly from the Stage's own synth cache. - **The Stage page-view button (A4 pages / Scroll / PDF pages) labeled itself with whatever mode was already active**, so it read as a passive status indicator rather than something clickable — confirmed live as genuine confusion: stuck in "Scroll" (one continuous page, no page breaks) with a button that said "Scroll" and nothing hinting that clicking it would do anything. It now shows what clicking it switches TO, the normal convention for a cycle button, with the current mode moved into the tooltip. ## [1.17.77] — 2026-07-24 ### Fixed - **Bulk "Auto-design voices" (and Assign/Generate images/Fix wrong-language voices) marked every just-created voice as "deleted from the Library — please reassign" the instant the run finished**, even though the voices were sitting right there. These bulk actions re-render the character table immediately after finishing, but never refreshed `window._voices` (the in-memory voice list, last loaded whenever the Voice Library page itself was visited) — so a voice created moments ago during the SAME run was judged against a stale snapshot that didn't know it existed yet, and got flagged as missing. Confirmed live: after deleting a batch of bad-accent voices and re-running Auto-design, every freshly-designed replacement showed the "missing voice" warning icon immediately, even though `/api/voices` already listed them correctly — a fresh page reload alone was enough to make the warnings disappear, confirming this was a stale-cache display bug, not a real data problem. The voice library now reloads right before that final re-render. - **Reusing a voice from "the same character elsewhere" (the fast path bulk designs use to keep a recurring character's voice consistent across a series) could hand out a voice that had since been deleted from the Library**, if some OTHER character record still referenced it and was never cleaned up. Now checks the voice still actually exists before treating it as reusable, same as the Library's own "missing voice" indicator already does everywhere else. ## [1.17.75] — 2026-07-24 ### Fixed - **The character-sheets progress box was back to leaving a huge empty area below its content, this time regardless of how much had actually been generated.** The previous fix (making the box shrink-to-content via `flex: 0 1 auto`) turned out to fight its own direct child, `.cs-progress-layout`, which is itself `flex: 1` (meant to fill whatever height the box has) — that made the box's "content size" circular, since it had no real minimum to shrink to, and it silently kept resolving to the full 90vh cap no matter how little content existed yet. Confirmed live: a result with only 8 characters found still stretched the box to the exact viewport-height ceiling, leaving a large empty gap below the small amount of real content. Reverted the box itself to `flex: 1` (fill the section, no gap below it — that was never actually the wrong part) and left the short-content problem to the preview/output split fixed earlier in 1.17.66, which doesn't have this circular-sizing conflict. Verified live at a realistic tall viewport: a short 8-character result no longer stretches the box, and a full 72-character list still scrolls correctly with no regression. ## [1.17.74] — 2026-07-24 ### Added - **Narrator paragraphs on the Stage page now have their own play button**, matching dialogue lines — previously only dialogue lines (`.reh-block`) had a play/pause control; narration paragraphs (`.reh-action-block`) had an edit button and a pre-synthesized dot but no way to play them individually. Deliberately kept plain rather than reusing dialogue's boxed/avatar/name-row treatment — just a small icon before the paragraph text, so narration keeps reading like narration instead of being visually pulled into a quote-like card. Reuses the same click wiring as the dialogue play button (`.reh-line-play-avatar`), so it needs no new playback logic. Verified live: clicking it correctly sets the active line and starts playback, same as a dialogue line's own button. ## [1.17.73] — 2026-07-24 ### Fixed - **The Stage/Perform & Export character sidebar never showed portrait thumbnails, even though the exact same characters show real photos everywhere else (Voices phase, Library).** Root cause: `renderCastList` (the "Who's playing which character?" panel) and `renderCastStrip` (the Stage sidebar) share ONE cached fetch of the book's character records, guarded by a single "already fetching this book" flag — but only `renderCastList` re-rendered itself when that shared fetch resolved. Entering Perform & Export borrows both panels at once, and `renderCastList`'s guard check usually won the race, silently claiming the fetch and leaving `renderCastStrip` with no way to know the data (with portraits) had actually arrived — it stayed on plain colored-letter dots forever, even though the cache had genuinely finished loading moments later. Both renderers now re-run whenever the shared fetch resolves, regardless of which one triggered it. Verified live: a 72-character sidebar went from 0 portraits to 29 real portraits rendering automatically on first entry, no manual action needed. ### Changed - **Character thumbnails in cast/roster lists (Stage sidebar, casting sidebar, recast picker) now show a 2px border in the character's own cast color**, matching the color-coding the plain letter-dot fallback already had — a real photo no longer loses that at-a-glance color identity in a long list. ## [1.17.72] — 2026-07-24 ### Fixed - **Batch-benchmarking a group of designed voices reported every single one as an error**, even though the voice_design engine itself was reachable the whole time. Root cause was server-side this time, in `core/tts_helpers.py`'s `_tts_benchmark_request`: it always built a generic voice_clone-style request against the one fixed TTS URL, with no awareness that a designed voice (no reference WAV) needs a completely different endpoint and request shape — the same one `/api/tts-preview`'s `backend=='voice_design'` path already uses successfully. Confirmed live: benchmarking a real designed voice ("EN_M_Junker") directly via `/api/voices/benchmark` failed with the voice_clone connection error until this fix, then succeeded (rtf 0.8, ok) once it routed through the correct engine. `_benchmark_voice` now checks each voice's own metadata (`origin`/reference-audio presence) before benchmarking and dispatches accordingly — genuinely cloned voices are unaffected and still correctly report the real voice_clone-backend outage rather than silently succeeding. ## [1.17.71] — 2026-07-24 ### Fixed - **The Voice Library table's own "Generate and play TTS preview" button had the same wrong-backend bug as 1.17.69's character-preview fix, in a different file.** It always synthesized through whatever engine the page-wide "Library TTS backend" selector happened to be set to — a deliberate override meant for bulk actions like Benchmark/Precompute where testing everything against one chosen engine on purpose makes sense — but applying that same override to a single voice's own row meant a designed voice failed outright unless the user had separately remembered to flip that selector to Voice Design first. Confirmed live: with the selector left on its voice_clone default, "EN_M_Junker" (a designed voice) failed with a voice_clone connection error even though Voice Design was reachable the whole time. Cloned voices still honor the selector (several backends can legitimately play a reference-WAV voice), but a designed voice's preview now always uses Voice Design regardless of what the selector is set to. Verified live: the same voice now plays successfully via "8021 Voice Design". ## [1.17.70] — 2026-07-23 ### Fixed - **Sparse/minor characters in an all-German (or any non-English) book could get auto-designed as English voices** — confirmed live on a real book: "Junker", "Kroah", "Leonardo" and a few other minor characters got British-flagged EN voices while every other character in the same batch correctly got DE. Root cause: `detectLang()` (utils.js) is documented to return `''` when there's too little text to be confident, and every one of its callers already treats a falsy result as "couldn't tell" and falls back accordingly — but the function itself violated its own contract and silently returned the literal string `'English'` instead. Since that's truthy, `_resolveBookLang()` (library-characters.js) — which exists specifically to catch exactly this case via a per-book majority-vote fallback across a character's siblings — treated the bogus 'English' guess as a confident, final answer and never got to use its own fallback. `detectLang()` now actually returns `''` on weak signal as documented. Voices already designed with the wrong language before this fix keep their existing (mistagged) id and aren't renamed automatically — only new auto-designed voices from here on are affected. ## [1.17.69] — 2026-07-23 ### Fixed - **A character's voice preview (▶ in the Cast table/profile) always tried to play through the voice_clone engine, even for a designed voice that has no reference WAV to clone from at all.** This "worked" only by coincidence whenever the voice_clone backend happened to be reachable, and broke for every voice — cloned or designed — the moment it wasn't, even though the designed voice's actual engine (voice_design) was up the whole time. Confirmed live: with the voice_clone backend down, previewing a designed voice ("Zerwas") failed identically to a real cloned voice ("Narrator"), even though voice_design itself responded fine directly. Now resolves the correct engine per voice (`origin === 'designed'` or no reference audio → voice_design, otherwise voice_clone) instead of hardcoding one for every voice. Verified live: the designed voice's preview now plays successfully; the genuinely cloned voice still correctly fails while its own backend is down, with no change needed there since that's a real infrastructure outage, not a code bug. ## [1.17.68] — 2026-07-23 ### Changed - **Concept art is now a full-width banner below the header instead of a small 110px thumbnail squeezed in next to the avatar** — too small to make out any real detail in a multi-pose design sheet. It now gets its own full-width section right under the header, sized up to 70vh, with the "Generate"/"Regenerate" button moved to a label row above it. - **The Perform & Export (Stage) character sidebar now shares the exact same search/sort styling as the Characters phase's sidebar.** Both already reused the same `.ab-cv-side` container, row markup, and collapse-to-avatars behavior, but Stage's search box and sort dropdown used their own separate, slightly different-looking CSS (`.reh-cast-side-tools` with bare, unclassed `input`/`select`) instead of the shared `.ab-cv-side-search`/`.ab-cv-side-sort` classes — a leftover inconsistency from when the two sidebars were built at different times. Switched Stage's markup to the shared classes and removed the now-unused duplicate CSS. Verified live: collapse/expand still works correctly on both. ## [1.17.66] — 2026-07-23 ### Added - **Concept art is now generated automatically for every character, and shown right in their profile.** Previously, the Concept Art Prompt (an auto-generated NPC design-sheet description) only ever produced text sitting in the Generation Prompts section — turning it into an actual image required noticing it, scrolling down, and clicking "Generate Concept Art" per character. The same background pass that already fills in `silly_tavern_prompt`/`concept_art_prompt` for a freshly cast book now also generates the image itself for every character with a prompt, right after casting finishes — no manual step needed. The result is displayed prominently in the character's profile page, right next to the avatar in the header, with click-to-enlarge and a "Neu generieren" button for regenerating it by hand at any time. Verified live: a real character ("Admiral Sanin") went from an empty "Kein Konzeptbild" placeholder to a generated design-sheet image shown in the header, openable full-size. ## [1.17.65] — 2026-07-23 ### Fixed - **The voice picker ("Auswählen" on a character card) could permanently show "No voices found" even though the voice library had loaded moments later** — it snapshotted `window._voices` once when opened and never looked again, so opening it before the library's background fetch finished (most likely reachable via Studio's Voices phase, which borrows the character grid without itself triggering a voice-library load) left it stuck empty for the rest of that popup's life, with no way to recover short of closing and reopening it. It now kicks off (or reuses) the voice-library load itself when it opens empty, and re-renders the list once that resolves. Verified live: a picker opened against a deliberately delayed/empty voice list showed "No voices found" and then correctly populated with all voices once the load completed, with no user action needed. ## [1.17.64] — 2026-07-23 ### Fixed - **1.17.60's `flex: 1` fix for the below-card gap created its own empty-space bug**: with a real, populated character list next to a short/not-yet-filled character card (e.g. right after selecting a character whose sheet hasn't generated much yet), the sidebar's genuine need for height (to show many rows with a working scrollbar) stretched the *preview panel* to match via the grid's `align-items: stretch`, leaving a large empty box below the short card's actual content — confirmed live with a 72-character sidebar next to a two-line preview (296px box, only 63px of real content). Root cause: `.cs-progress-preview` had `flex: 1`, forcing it to grow and fill whatever height its sibling column demanded, even when it had nothing to show there. Changed the preview panel to shrink-to-its-content (floored at its existing 220px minimum, so it doesn't look collapsed when truly empty) and let `.cs-progress-output` (the passage/live-output pane, which already scrolls its own content and existed for the "growing" case anyway) absorb any leftover column height instead. Also confirmed (separately, via the same live test) that `.cs-progress-box-big`'s own height needed an explicit `height: auto` override — a more specific scoped rule that only set `flex: 0 1 auto` was silently losing to an unscoped, unrelated rule's explicit `height` elsewhere in the stylesheet, since CSS cascade resolves per-property, not per-rule. Verified live: preview panel now matches its actual content instead of stretching, while a 72-character sidebar list still scrolls correctly (2526px scrollHeight vs. 454px clientHeight). ## [1.17.61] — 2026-07-23 ### Fixed - **Clicking a character in the sidebar no longer scrolled to their actual first line** — a real regression from the earlier `content-visibility: auto` performance fix (1.17.49). The selection/data logic itself was correct (confirmed live: the right dialogue row was genuinely focused), but off-screen pages' heights are only an ESTIMATE (`contain-intrinsic-size`) until the browser actually measures them, so a single `scrollIntoView()` computed against a target many pages away could land wherever the sum of all those estimation errors put it — confirmed live at over 4000px off-screen. Every `scrollIntoView` call in the casting feed now does an instant rough pass followed by a corrected smooth pass once the browser has actually laid out whatever came into view, without giving up the original performance win. Verified live: the target row went from ~4098px off-screen to correctly positioned inside the visible feed. ## [1.17.60] — 2026-07-23 ### Fixed - **Still wasted empty space below the character-sheets card after 1.17.59** — that fix matched the two internal columns to each other, but the whole card was still capped at a fixed viewport-relative height, while its section (`#s-reader`/`#s-caststudio`) is a fixed, full-height flex column regardless (needed for other content like the PDF reader). Any time the card's own capped height came in under the section's real height, the leftover section space showed up as dead area below the whole card, not just between its columns. Switched to `flex: 1` (the same pattern `#reader-main-view` already uses under these same two sections) so the card actually claims whatever height the section provides instead of guessing a fixed number. Verified live: section/panel/box all measure identically now (747px), with zero leftover gap. ## [1.17.59] — 2026-07-23 ### Fixed - **The 1.17.56 sidebar-scrolling fix introduced a new mismatch: a large empty gap below the shorter of the two columns.** Giving the sidebar a fixed height cap independent of the main passage/output column meant the two no longer matched — when the passage side was short (e.g. still on passage 1, or mid network-error-retry) while the sidebar's cap stayed constant, the shorter column left dead space below it. Replaced the fixed cap with the same bounded, viewport-relative height the floating-overlay version already uses, so both columns stretch to match the SAME real height via the grid and scroll independently within it — no constant on either side to drift out of sync with the other. Also removed an explicit `height: 100%` on the sidebar that created a circular sizing reference against the grid's own auto-sized row (a child asking for "100% of a row whose height is itself content-derived" resolves back to the child's own content size, not the row's actual height) — grid's `align-items: stretch` already gives it a correct, definite height with no percentage math involved. Verified live: both columns now measure identically (641px each) instead of one leaving unused space. ## [1.17.57] — 2026-07-23 ### Fixed - **Every line in the casting feed could get silently rendered twice** — the same segment appearing as two separate (but identical, same underlying object) DOM rows. Confirmed live: a character's line count in the top per-character navigator ("bar-pos") read exactly double what the sidebar correctly showed, and clicking a character in the sidebar no longer scrolled to their actual first line, since the duplicate rows threw off which DOM node was really "first" in reading order. Root cause: the feed's chunked redraw had no protection against two overlapping calls — each one's own in-flight batch loop kept running independently of the other's `feed.innerHTML = ''`, so two legitimate (non-recursive) triggers close together both ended up appending their own full copy of every row. A generation token now lets an in-flight redraw notice it's been superseded by a newer one and stop instead of racing it. Verified live: a 1598-segment feed went from 1500 (partially duplicated, inconsistent) rows to exactly 1598, with a 1:1 match between DOM rows and segments for every character checked. ## [1.17.56] — 2026-07-23 ### Fixed - **The character-sheets generation progress view's own "Characters found" sidebar had the same no-scrollbar symptom as 1.17.55, but a different root cause.** When this panel is embedded inline in the page (Read Aloud / Studio) rather than shown as a floating overlay, its outer box deliberately gets `height: auto` so it grows naturally with the page — but that removes the one bounded ancestor the sidebar's whole flex/overflow chain needs to scroll internally. Gave the sidebar its own independent height ceiling for this specific context, regardless of what the main passage/output column is doing. Verified live with a real 72-character list: went from zero usable height to scrolling correctly within a bounded ~640px box. ## [1.17.55] — 2026-07-23 ### Fixed - **The "Characters found" sidebar on the casting view had no scrollbar**, leaving a large mismatched empty gap in the feed column next to it. `.ab-cv-chars` (the scrolling list itself) was missing `flex: 1` — without it, it just sized to its own content like any other block element, so its own `overflow-y: auto` never had a bounded height to actually scroll within; the parent's `overflow: hidden` just silently clipped it with no scrollbar shown, while the grid row stretched to match the unbounded content anyway. Verified live: a 72-character list went from growing past its container (scrollHeight 3452px vs. a 404px box, no scrollbar) to scrolling correctly within it. ## [1.17.54] — 2026-07-23 ### Added - **Concept art can now actually be generated as an image**, not just text. The "Character Concept Art Prompt" box previously had no image action at all (only Copy/Regenerate for the prompt text itself) — added a "Generate Concept Art" button that calls the same image-generation pipeline as the profile portrait, storing the result alongside the prompt (never overwriting the character's main portrait) and showing it inline in the box. ### Changed - **Rewrote the concept-art prompt** to follow a structured Task/Subject/Context/Style/Composition/Lighting/Constraints/Output pattern (adapted from a user-supplied reference), producing a proper NPC/character reference sheet: full-body front/side/back views plus an isolated callout row for the character's own established props/costume pieces (only when the profile actually has any — no invented gear), with explicit genre/era grounding and an original-character/no-copyright disclaimer. Verified live against a real character — produces exact prop counts and setting-appropriate detail instead of the previous generic single-sentence version. ## [1.17.53] — 2026-07-23 ### Changed - **Rewrote the "SillyTavern Character Prompt" generation.** It was only ever built as a few short labelled lines (Description/Personality/Scenario/First message/Example dialogue) via a generic instruction — now generates a full structured SillyTavern character card (Personal Information, Appearance, Personality, Likes, Dislikes, Goals, Skills, Weapons, per-occasion Outfits, gender-aware), followed by an open-ended Scenario block and a First Message, adapted from a user-supplied prompt template. Since the character's full profile is already known from the book (unlike the original template, written for building a card from scratch via internet research), the model is told to use ONLY that profile instead of searching fandom/wikipedia, marking anything genuinely inferred with a trailing `*`. Bumped this field's own token budget (1536 → 3000) since a full card no longer fits the old ceiling. ### Known limitation - The local model sometimes stops right after the Scenario block and skips the First Message despite an explicit instruction to include it — a model-following limitation, not a formatting bug. Worth another look if it's consistently a problem. ## [1.17.52] — 2026-07-22 ### Fixed - **Found and fixed a real infinite-recursion bug introduced in 1.17.45**, confirmed live via the browser's own renderer process sitting pegged at steady high CPU (not idle — a genuine runaway loop, not a hang): `audiobookCastView()` calls `navReaderView('cast')` (an alias for `showReaderView`) as part of its own normal setup — which is exactly the function 1.17.45 wrapped to keep Studio's sub-tabs in sync. That wrapper called `_stuShowCastView('identify')`, which calls `audiobookOpenCastView()`, which calls `audiobookCastView()` again, which calls `navReaderView('cast')` again — forever. This reliably reproduced entering Studio's Characters tab on a book with an existing cast. Fixed with two guards: never re-enter the wrapper while already inside a call it triggered, and skip entirely once Studio's sub-tab already matches the requested view (the overwhelmingly common case this was needlessly re-triggering on every call). Some, but likely not all, of the freezes reported since 1.17.45 were probably this. ## [1.17.51] — 2026-07-22 ### Fixed - **Extended the content-visibility fix to the character-cards grid** (`.lib-char-card`, shared by Library's Cast page and Studio's Characters tab — same borrowed elements, not separate code). A production with dozens of characters, each carrying a generated portrait image, stayed fully laid out and painted at once with no virtualization — the same category of issue just fixed for the casting view, likely also in play during bulk profile/portrait/voice generation across a large cast. Same proven technique, same guarantee: off-screen cards stop costing layout/paint, nothing else changes. ## [1.17.50] — 2026-07-22 ### Fixed - **Reproduced and fixed the actual cause of the recurring tab freeze/crash**, verified live: restoring a fully-cast, 235-page book kept all ~40,000 DOM nodes (every narration/dialogue row for the whole book, by design — so the full cast stays available for review) fully laid out and painted at once, even the ~99% currently scrolled out of view. Confirmed measurably: before this fix, scrolling the restored view took real, non-trivial time; after, scrolling to the very bottom or top of a 1598-segment cast is instant (0ms). `content-visibility: auto` (the same technique already used elsewhere in this app for other large lists) now lets the browser skip layout/paint work for off-screen page groups entirely — the data and every existing feature (search, click-to-edit, page navigation) are completely unaffected, since the nodes are still there, just not actively rendered while off-screen. ## [1.17.49] — 2026-07-22 ### Fixed - **Casting a long book could hang and crash the tab partway through** (confirmed live: crashed around page 33 of a 235-page book, 1598 segments in). Two compounding O(n²) costs in the live casting view: (1) the feed's own housekeeping ran a full `querySelectorAll` over every row/note/divider in the whole book-so-far on every ~80-segment batch, for a trim limit that's permanently disabled (infinite) — pure wasted work that grows with the book; (2) the autosave fired after every single chunk, re-stringifying and both localStorage-writing and network-sending the ENTIRE accumulated segments array every time, also growing with the book. Both scale with total segments cast so far, not a flat per-page cost, so the first ~30 pages felt fine and it degraded from there. The feed query is now skipped entirely since it could never do anything anyway, and autosaves are throttled to at most once every 4 seconds (always still saving on completion, so nothing is lost at the end of a run). ## [1.17.48] — 2026-07-22 ### Fixed - **PDF zoom hang persisted even with the render timeout from 1.17.47.** That confirms the hang is likely a synchronous main-thread block (e.g. PDF.js decoding an embedded image at its own native resolution before any downscaling) — nothing awaitable, including a timeout, can rescue a hang that already owns the thread before our code gets a chance to run. Since shrinking the *output* canvas doesn't help if the cost comes from the *source* image's own resolution, the safety budget is now much smaller (~2MP / 1800px per side, down from ~6MP / 3000px) so the requested render scale for an oversized page stays low enough to avoid the pathological decode in the first place, regardless of the zoom level shown in the UI. ## [1.17.47] — 2026-07-22 ### Fixed - **The previous canvas-size clamp (1.17.46) alone wasn't enough to stop a page hanging the tab.** A single oversized dimension can be a problem even under the area budget on some browsers, and — more importantly — the page render itself can still hang regardless of the requested output size (e.g. an embedded image codec that decodes at native resolution before any downscaling happens). Added a hard per-side cap alongside the area budget, and — the actual guarantee this time — a timeout around the render call itself: a page that doesn't finish rendering within 15s is cancelled and left for a later retry (e.g. scrolling away and back) instead of hanging the tab indefinitely. ## [1.17.46] — 2026-07-22 ### Fixed - **Zooming a PDF page in (a cover page in particular) could hang and then crash the entire browser tab** (Chromium "Aw, Snap! SIGTRAP"). Page canvases were sized directly from page size × zoom with no upper bound — a page with an unusually large intrinsic size (some scanned/cover pages) at ~400% zoom could demand a multi-hundred-megabyte backing buffer. The actual rendered resolution is now capped to a safe pixel budget regardless of zoom or page size; the canvas's on-screen (CSS) size still matches the requested zoom exactly, so a single oversized page just renders a touch softer instead of crashing the tab. ## [1.17.45] — 2026-07-22 ### Fixed - **"New recast (discard & rebuild all)" (and other actions that call `showReaderView` directly) left Studio's Characters tab completely blank**, with the LLM pass clearly running in the background (GPU busy) but nothing visible at all. Studio shows/hides the borrowed casting panels one level up via its own slot visibility, in lockstep with its "Identify Characters"/"Cast Characters" sub-tabs — but `showReaderView('cast'|'chars')` toggles `.hidden` directly on the panels themselves, one level down, with no awareness of Studio's slots. If the two disagreed (e.g. recast was triggered while on the wrong sub-tab), the panel actually holding the live progress UI ended up hidden inside its slot while the other, now-empty panel's slot was the visible one — a blank screen with real work happening underneath. `showReaderView` now keeps Studio's own tab UI in sync whenever this happens. ## [1.17.44] — 2026-07-22 ### Fixed - **Speaker attribution left too many lines as "Unknown"** on cues a human reader would catch instantly: self-introductions ("Man nennt mich Andra"), and idiomatic narration that doesn't use a literal speech verb ("Marcian fand als erster seine Stimme wieder", "ihre ersten Worte waren..."). The LLM attribution prompt now has explicit rules for both (self-introduction, and a widened voice-announcement rule covering these idioms), and the deterministic code-level fallback — which already exists because the LLM alone measurably still misses ~44% of mechanical patterns even when the prompt spells them out — now catches these same patterns directly instead of only literal speech-verb tags. ## [1.17.43] — 2026-07-22 ### Fixed - **PDF import could get permanently stuck on one page** ("Reading PDF… page 224/235" that never advances). The per-page text/heading extraction loop had no timeout anywhere — a hung (not rejected, just never-settling) page render or Tesseract OCR call, most likely on an image-heavy or blank page, silently stalled the entire sequential import with no error and no way to recover short of reloading. Page render, heading OCR, and plain text extraction now all race against a timeout and skip to the next page (logging a warning) instead of hanging forever. ## [1.17.42] — 2026-07-21 ### Fixed - **"Voice design failed... produced broken audio after 3 attempts" fired on perfectly good voices.** The retry logic's "bad" check included the benchmark's `realtime_ok` flag, which also goes false whenever the designed voice's own reference audio (built from the character's sample text — often a real book quote) simply runs longer than 25 seconds, a length/performance advisory with nothing to do with audio corruption. Since sample length barely changes between retries, this deterministically failed all 3 attempts for any character with a longer line, wasting 3 generations and then refusing to assign any voice at all. Only real defects (synthesis error, output clipped at the max-duration guard, implausible wpm) count as "bad" now. ## [1.17.41] — 2026-07-21 ### Added - **Per-book context (genre, setting, era, language)**, editable via a new "Context" button on each production — e.g. "High fantasy, like Lord of the Rings, medieval times, German". Feeds directly into every voice design prompt for that book, and is now the authoritative language source for language-matching/mismatch checks, instead of guessing from one character's own (often sparse) sheet text. ### Fixed - **Bulk "Auto-design voices" silently did nothing for characters whose name is a known alias of another character** (e.g. "Garbaz" as an alias of "Arthag") — the save step's identity-matching redirected the update onto the alias's canonical record instead of the character actually selected, so the selected row never got a voice while a seemingly unrelated character's voice silently changed instead. This is a known limitation of the alias-based identity system, not yet fixed at the root — but is now at least understood; a real fix needs to distinguish "explicit per-card action" writes from freeform sheet-merge writes. - **Sparse/minor characters ("Bote", "Frau", "Mann", generic crowd roles) with no descriptive sheet text kept getting voices designed in English by default even in all-German books**, because the per-character language detection this relied on has nothing to detect from on a near-empty sheet. Now falls back to a majority vote across the character's own book siblings, and — with the new book context above — to an explicit setting first. - **A voice that failed the benchmark on all 3 retry attempts was still committed as the character's final voice anyway** (with only an easy-to-miss toast), which is exactly how several visibly-broken voices (0.02x factor, 1875 wpm) ended up in the library despite the retry logic added in 1.17.36. It now fails the character outright instead — leaving whatever voice it had before (or none) untouched — rather than silently swapping in audio already proven broken. ## [1.17.40] — 2026-07-21 ### Fixed - **Deleted voices could reappear on their own.** The background voice-index rebuild (a full disk rescan that can take a noticeable while over 100+ voices) replaces the *entire* index with what it found once it finishes — if a voice was deleted while that scan was still in progress, its row was still sitting in the scan's snapshot from before the delete, so the rebuild resurrected it the moment it finished. This got much easier to hit now that voice-design retries poll the backend far more often. The scan now re-checks each file still exists immediately before writing, closing the window down to next to nothing. ## [1.17.39] — 2026-07-21 ### Fixed - **Restarting the TTS backend (including the new automatic post-design restart added in 1.17.36) blocked the entire app server for the whole restart duration.** `/api/tts/restart` and the local-container start/stop/restart endpoints called a raw blocking Docker-socket request directly inside their async route handlers instead of off-thread — with two TTS containers configured, that's up to ~20s where *every* other request (including a plain character list fetch) just hung. Confirmed live: switching to Studio's Voices tab during a bulk voice-design run showed "No characters yet" for a book that very much had characters, because its own character fetch got starved by an in-flight restart. All Docker socket calls in these routes now run in a worker thread instead of on the event loop. - **A failed character-list fetch (of any cause) blanked out an already-populated list instead of leaving it alone.** `libraryRenderCharacters()` now fetches before touching the DOM, so a transient failure just leaves the current view in place (with an error toast) rather than replacing real data with an empty state. ## [1.17.38] — 2026-07-21 ### Fixed - **Every action on the Characters/Cast page (remove voice, auto-design, delete, etc.) jumped the page back to the top.** The full-list rebuild those actions trigger briefly collapses the list down to a one-line loading placeholder, which the browser responds to by clamping the scroll position back to fit — confirmed live as a scroll-to-top after every single click. The page's scroll position is now restored once the rebuilt list is back in place. ## [1.17.37] — 2026-07-21 ### Fixed - **Deleting a broken voice from the Voice Library left every character still pointing at it, with no indication anything was wrong.** Character cards, the table view, and the character detail modal now check each assigned voice id against the loaded voice list and mark it red (with an alert icon and an explanatory tooltip) when it no longer exists, instead of silently showing a dead voice id as if it were still valid. ## [1.17.36] — 2026-07-21 ### Fixed - **Designed voices could end up permanently broken (near-silent/truncated output, or absurdly slow to render) with no automatic detection.** Confirmed live across a whole book's cast: some designed voices measured impossible speech rates (6 wpm, 5625 wpm) or failed the realtime benchmark outright. Auto-design/redesign now benchmarks each generation attempt (under a disposable id, never the character's real voice) before committing it, and automatically deletes and retries up to 3 times if the result is corrupted, clipped, or outside a plausible 80–400 wpm range; if all attempts still fail, the character keeps the last attempt and a toast flags it for manual review. - **Overwriting an existing voice's audio during a redesign never flagged the TTS backend as needing a restart**, unlike the equivalent "replace audio" path — so a freshly redesigned voice could silently keep serving the *old* cached reference audio during synthesis, with no warning shown anywhere. `/api/save` now sets the same `needs_tts_restart` flag `/api/voice-replace` already did. - **Restarting the TTS backend after a redesign now happens automatically** instead of relying on the user to notice the warning and click "Restart TTS" themselves — once after a single redesign, once after a whole bulk batch (not per-voice, to avoid restarting a model server dozens of times in one run). ### Security - **Voice reference photos could be silently and permanently overwritten with no backup**, discovered after a real character-portrait image overwrote a cloned narrator's actual reference photo with zero way to recover it. Picture uploads now back up any existing picture before overwriting it (mirroring the existing audio backup pattern), so future overwrites are recoverable even when triggered by a bug. ## [1.17.34] — 2026-07-21 ### Changed - **Voice design prompts now demand specific, differentiating detail instead of generic category labels.** "Young female voice, energetic tone, clear pitch" describes a whole demographic, not a person — confirmed live as the real cause of several same-age/gender characters sounding near-identical. Both the LLM-generated prompt and the client-side fallback (for sparse profiles with none of the richer sheet fields filled in) now push for a specific timbre, pace/rhythm quirk, and character-specific emotional baseline; the fallback also gets a deterministic per-character variety injection so even the sparsest profile differs from every other same-gender character instead of using the identical generic sentence. - **"Design a voice" (single-character, manual flow) now asks before silently reusing a series voice**, with a way to hear the existing voice first — confirmed live as confusing (clicking "design" expecting something new, silently getting a reused voice instead with only an after-the-fact toast). Choice of using the existing voice or designing a genuinely new one. Bulk actions are unaffected — they keep the fast, silent reuse-first behavior a 40-character run needs. ## [1.17.33] — 2026-07-21 ### Fixed - **"Rebenchmark this one" crashed with "Cannot set properties of null (setting 'className')" from the single-voice detail view.** Opening a voice in the inspector physically moves its benchmark chip element out of the row and into the inspector panel — the rebenchmark handler was still looking for it in the old (now-empty) location. Falls back to the inspector's own copy, and no longer crashes if it genuinely can't find either. ## [1.17.32] — 2026-07-21 ### Added - **"Remove voice" button** in the character table — clears the assigned voice, leaving the character unassigned (e.g. before picking or designing a replacement). - **"Fix wrong-language voices" bulk action** — scans every character in a production (no selection needed) for a voice whose language doesn't match the book's own, and designs a properly-matching replacement for each. Cleans up characters assigned before the language-matching fix (1.17.27) existed, or from an older bulk run. - **"Edit voice design prompt" button** — opens the Design a Voice page pre-filled with the character's Voice Design Prompt (name, gender, language, and prompt text), so it can be reviewed/adjusted before regenerating, instead of only ever getting an instant, un-editable auto-design. - **Hovering a character's profile picture now shows a large (512×512) preview** near the cursor — the table/card thumbnail alone (often just 32-40px) gave no real sense of the actual portrait. ## [1.17.31] — 2026-07-21 ### Added - **A dedicated "design a new voice" button in the character table**, for when the current voice just doesn't fit — unlike "Auto" (which prefers reusing an existing/series voice first), this always generates a genuinely fresh voice, bypassing any reuse/match logic entirely. ### Fixed - **The voice play button's horizontal position varied row to row** depending on how long the voice name was, since it sat on the same row as the name. Moved to the second (action) row, so it lines up consistently under every row's voice name regardless of length. ## [1.17.30] — 2026-07-21 ### Fixed - **Long voice names pushed the "Auswahl"/"Auto" buttons out of the visible column in the character table.** The voice name, play button, and both action buttons were all one row in a fixed-width column — confirmed live, a longer voice id left the buttons cut off entirely. Now two stacked rows: voice name + play button on top, Auswahl/Auto underneath. ## [1.17.29] — 2026-07-21 ### Changed - **Voice previews and voice-design samples now include a real line from the book, not just a bare greeting.** The quick preview button ("Hallo, ich bin NAME") and the actual voice-design generation step now both append an actual line the character speaks (from the current audiobook's attributed dialogue, or the character sheet's own quoted sources) when one's available — much better preview of how the voice actually sounds reading the book than a name-only greeting alone. ## [1.17.28] — 2026-07-21 ### Fixed - **"Auto-design voices" silently did nothing for a character that already had a voice.** The series-reuse check (`_findVoiceFromSameCharacterElsewhere`) runs before generation to avoid burning a fresh design call for a recurring character — but since it naturally finds the character's own most-recent record first, clicking design on an already-voiced character just re-saved the identical voice id and returned, never actually generating anything new. Now skipped specifically when the "reuse" would be a no-op (matches what the character already has); genuine cross-book reuse (a distinct voice this record doesn't have yet) still applies as before. ## [1.17.27] — 2026-07-21 ### Fixed - **Auto-designed voices all sounded nearly identical regardless of character.** The shared TTS "stability" defaults (fixed `seed: 0`, `temperature: 0.1`) were applied to voice DESIGN calls too, not just voice CLONE calls — sensible for clone (consistent retakes of the same voice), backwards for design (every call should produce a different voice from a different prompt). Pinning the random draw meant the character's own prompt text became the only source of variation, and low temperature flattened even that. Voice design now uses the backend's natural per-call randomization, same as other creative-voice backends. - **Existing-voice reuse/name-matching ignored language, producing accent mismatches.** Confirmed live: English-designed voices got assigned to German-book characters via name-substring matching. Neither the series-reuse check nor the new name-matching check verified the voice's language (encoded only as an `EN_`/`DE_`-style id prefix, the only place language is ever recorded) against the book's own detected language. Both now skip a same-name match in the wrong language and fall through to designing a fresh one instead. - **Character portrait/profile image prompts had no genre/setting anchor**, so a fantasy book's military-sounding occupations ("Admiral", "General", "Prinz") could render as modern-day imagery. Both the LLM-generated prompt and the client-side fallback now explicitly work out and state the story's genre/era, and instruct against real-world modern anachronisms unless the story is actually contemporary. ## [1.17.26] — 2026-07-20 ### Changed - **"Auto-assign selected" now designs a bespoke voice per character instead of handing out an arbitrary same-gender pick.** Previously it just filtered the whole voice library by gender and grabbed the highest-rated match, with no check that the voice had anything to do with the actual character — confirmed live: a 46-character bulk assign returned existing voices for every single one, several clearly generic. Now: reuse a voice from the same-named character elsewhere in the series (unchanged) → otherwise use a voice already in the library whose own name matches the character's (e.g. "DE_M_Zerwas" for a character named "Zerwas") → otherwise design a new voice from that character's own profile. ## [1.17.25] — 2026-07-20 ### Fixed - **Voices phase could auto-expand the wrong book.** Scoping which book's section shows expanded read `readerState.title`, which is left pointing at whichever book was last opened via the Source tab — not necessarily the book whose cast is actually active. Confirmed live: navigating to Voices via "View cast" from a book 2 casting session expanded book 1's section instead. Now resolves the title from `_audiobook.bookId` (kept correctly in sync with the active cast session) via the server, falling back to `readerState.title` only when no cast session is active. ## [1.17.24] — 2026-07-20 ### Fixed - **"View cast" (Cast ⌄ menu, Characters tab) did nothing when clicked in Studio.** Same root cause as the earlier "Open Script Rehearser" fix: it called `navTo('s-library')` directly, silently swallowed by Studio's nav guard while Studio is the active section. Redirects to Studio's own Voices phase instead, which already shows the same cast/character roster. ## [1.17.23] — 2026-07-20 ### Added - **Studio's phase tabs (Source/Characters/Voices/Perform & Export) didn't read as a sequential flow.** The old cross-section "Previous/Next" stepper was deliberately removed from Studio in 1.17.8 (it only knew about the old sections, not Studio's own phases), but nothing took its place — the only way to move forward was clicking a tab directly, with no visible "what's next" affordance. Each tab is now numbered (1-4), and a Studio-specific Previous/Next button pair sits in the header, disabled at the first/last phase. ## [1.17.22] — 2026-07-20 ### Fixed - **Studio's Characters tab could end up with no navigation controls visible at all.** The relocated dropdown toolbar (1.17.15) only ever hid the plain "Identify Characters"/"Cast Characters" tabs once a footer was found to replace them — a later transient state (e.g. "Checking saved cast before starting a new one…") rebuilds the panel without any footer at all, and since nothing re-showed the plain tabs, confirmed live: neither the dropdown toolbar nor the plain tabs were visible, leaving no way to navigate. The plain tabs now fall back to visible any time there's genuinely no footer to relocate, not just the first time. ## [1.17.21] — 2026-07-20 ### Fixed - **Exported audiobooks were only playable for the first few seconds, despite a correctly-sized file.** `audiobookExport()` synthesized every line as an independent mp3 clip and merged chapters by naively concatenating the raw mp3 byte streams (`new Blob(blobs, {type:'audio/mpeg'})`) — each clip carries its own frame/ID3 headers, which most players decode only the first of before stopping or glitching (confirmed live: an 85MB export that reported as 22 seconds playable). Also confirmed the per-line mp3 encoding was never given an explicit bitrate, silently falling back to ffmpeg/lame's low default (32kbps) rather than any bitrate this app actually chose. Fixed at the source: lines now synthesize as lossless WAV, merge properly per chapter via the same correct PCM-concatenation helper already used elsewhere (`mergeWavBlobs`), and get one real mp3 encode pass server-side at an explicit 96kbps (matched to the engine's native 24kHz mono output — higher would just be wasted file size, not more real quality) via a new `/api/audio/encode-mp3` endpoint. ## [1.17.20] — 2026-07-19 ### Fixed - **Narrator's voice never reached synthesis even when properly assigned in the Library.** "Synth all" in full-audiobook mode silently synthesized only the ~380 dialogue lines instead of all ~1325 narratable lines, with no error — the narrator's cast slot uses an emoji-prefixed sentinel key internally (`📖NARRATOR`), and `rehApplySharedCast()`'s roster lookup lowercased that whole sentinel (`"📖narrator"`) instead of translating it to the library's plain `"narrator"` key first, so the match always silently missed and the library's assigned voice never populated `rehState.narratorVoice` — the exact field `synthAll()` checks to decide whether to include narration at all. ## [1.17.19] — 2026-07-19 ### Fixed - **Reverted 1.17.16 through 1.17.18's timeout tuning on the character-sheets fallback request — it was solving the wrong problem.** Confirmed by comparing against a still-running, still-working session on a different book: the streaming endpoint never delivers a single visible delta for this generation (reproduced identically at 60s/300s/560s — not a matter of waiting longer), and the plain fallback request reliably completes in a couple of minutes *when left alone*. Adding a timeout to that fallback (as 1.17.16 did) meant every abort orphaned a thread holding the shared LLM lock (the underlying call can't be interrupted by a dropped connection), so the next attempt just queued up behind it and repeated the same doomed cycle — the timeout was actively causing the stall it was meant to prevent. The fallback is unbounded again, matching the working reference behavior; the stream's own idle-timeout is back to 60s since there's nothing to gain from waiting longer on an endpoint that never streams anyway. ## [1.17.18] — 2026-07-19 ### Fixed - **1.17.17's 300s timeout still aborted a genuinely-completing character-sheet passage, twice in a row — including immediately after a full restart with no pre-existing stuck threads.** Confirmed live: this client-side abort doesn't actually stop the server from working — the backend's upstream call sits in an uninterruptible blocking socket read, so the abort just orphans a server-side thread that keeps holding the shared LLM lock until its own ~600s watchdog force-closes it, and the next retry then queues up behind that same abandoned thread. A short client timeout doesn't recover faster here, it actively makes things worse. Both timeouts raised to 560s — close to, but safely under, the server's own watchdog ceiling — so aborting stays a last resort instead of routine, and any abort that does happen leaves the orphaned thread already near its own expiry. ## [1.17.17] — 2026-07-19 ### Fixed - **1.17.16's timeout fix was too aggressive and made things worse.** Confirmed live via the LLM server's own logs: a single character-sheet passage can run continuously for 4.5+ minutes with steady output the whole time — it was never stuck, it just doesn't emit a visible streamed delta until reasoning is done. The 60s/90s timeouts from 1.17.16 were aborting genuinely-succeeding generations mid-flight, discarding real progress and retrying from scratch — which looks identical to a real stall (repeated timeouts, zero progress) from the outside. Both timeouts raised to 300s, comfortably above the observed real duration and still safely under the server's own ~600s watchdog. ## [1.17.16] — 2026-07-19 ### Fixed - **Character sheet generation could freeze indefinitely on one passage with zero feedback.** Confirmed live: stuck on "Passage 2/114…" for 7+ minutes with no new requests, no error, no retry. Root cause: the streaming request has its own 60s idle-timeout, but the non-streaming fallback it falls back to on a stream failure had no timeout at all — when the backend's LLM lock was held by an abandoned stream thread stuck in a blocking socket read (a pre-existing server-side condition, only self-healing via a ~600s watchdog), the fallback request just hung for up to 10 minutes instead of failing fast into the existing retry-with-backoff logic. The fallback now aborts after 90s and retries like any other network error. ## [1.17.15] — 2026-07-19 ### Fixed - **Studio's Cast Characters tab pointed at the wrong pass entirely.** There are two unrelated things in the old app both loosely called "casting": dialogue speaker-attribution (Identify Characters tab, already wired) and passage-by-passage character PROFILE generation (appearance/backstory/voice notes, with live "Passage N / M…" progress and a per-character progress sidebar) — the old page's optional WF_STEPS step 3. An earlier fix (1.17.4) redirected this tab's empty-state button at the speaker-attribution menu instead, so it never actually offered profile generation at all, appearing to just go blank. Now calls the real entry point (`csForReader()`) directly, rendering the same live-progress UI the old page has, inline. ## [1.17.14] — 2026-07-19 ### Fixed - **Unknown-speaker dialogue whose preceding narration named the speaker with a plain-period inquit ("Garbaz rief von unten herauf.") stayed unresolved.** `audiobookResolveUnknowns`'s existing rules only recognized a preceding inquit when it ended in a colon ("... sagte:") or when the speech tag came right after the quote — a Name + speech-verb sentence ending in a normal period, especially with extra words between the verb and the period ("...Ork legte den Kopf in den Nacken und schrie seinen Triumph zum Himmel."), matched neither. Added a third rule that checks the preceding narration's last sentence against the actual character roster (not a generic capitalized-word guess), so multi-word names/aliases resolve correctly too. ## [1.17.13] — 2026-07-19 ### Fixed - **LLM casting could leave real dialogue lines completely unattributed right next to correctly-cast ones in the same passage.** Confirmed live: "»Ich glaube, ich bin in dich verliebt.«" got correctly split and attributed to Alrik, while "»Halt, bleib stehen.«" and "»Ich liebe dich«" a few lines later stayed silently merged into narration with no speaker at all — same passage, same `»...«` markers. The deterministic backfill that's supposed to catch dialogue the LLM misses only checked "did this chunk produce *any* dialogue at all" (`.some()` over the whole chunk), so it was satisfied by the first correct split and never re-examined the rest. Every leftover narration segment is now individually re-scanned and re-split. Already-cast passages need "Continue uncasted" or a targeted recast to pick this up — it doesn't retroactively repair segments already saved to a draft. ## [1.17.12] — 2026-07-19 ### Fixed - **No visible way to import a document after deleting the current one.** The paste/drag-and-drop import controls live inside the "Voice & synthesis settings" card, which is collapsed by default — fine once a document is loaded, but confirmed live as a dead end right after deleting the current book: an empty page with nothing indicating the import controls were hidden inside that collapsed card above it. The card now force-expands whenever no document is currently loaded (without touching a user's own collapse preference once one is). ## [1.17.11] — 2026-07-19 ### Fixed - **Narrator scare-quotes (›Zelt‹) got split out as speakerless "dialogue" lines.** German prose uses single guillemets ›...‹ for the narrator ironically/emphatically quoting a word within narration (e.g. "...die die Orks aufgeworfen hatten. ›Zelt‹ war eine sehr schmeichelhafte Bezeichnung..."), distinct from »...« for actual spoken dialogue — but the deterministic quote-splitter treated both the same way, producing standalone "Unknown"-speaker fragments for just the quoted word (confirmed live on book 2: "Zelt", "verlausten Pony", "Skipperedikt", "Seulaslintan", among others). Single guillemets are no longer treated as a dialogue delimiter anywhere in the casting pipeline. Already-cast books need their affected passages recast (e.g. "Continue uncasted" or a full recast) to pick this up — existing casts aren't retroactively repaired. ## [1.17.10] — 2026-07-19 ### Fixed - **Relocated Cast Characters toolbar (1.17.9) reappeared as a stale duplicate at the bottom after any in-panel edit.** `audiobookCastView()` rebuilds the whole panel (a fresh `#ab-cv-foot`, same id) on every call, not just the first — confirmed live: after using "Split text to Unknown Speaker" to edit a line, a second, un-relocated footer (with "Open Script Rehearser" showing again) appeared back at the bottom while the relocated copy stayed at the top. Replaced the one-time move with a `MutationObserver` that catches every rebuild, not just the first. - **Leaving Studio for the old Read Aloud page would come back missing its own footer.** The relocated toolbar was never part of the whole-panel borrow Studio already tracks for returning things to where they came from — it's now explicitly restored inside the panel before the panel itself goes home. ## [1.17.9] — 2026-07-18 ### Fixed - **Character portraits fetched and embedded as raw base64 on every bulk character-list load, blocking rendering with no feedback.** `/api/characters` (used to build the Cast Characters grid, the Library grid, and elsewhere) returned every character's full base64 portrait inline; for a book with dozens of generated portraits this made the JSON payload and the resulting `innerHTML` write tens of megabytes, so Studio's Cast Characters tab could sit completely blank for a long time with no spinner — easy to mistake for broken. The list endpoint now returns a lightweight `/api/characters/{id}/image` URL instead of the raw blob (single-character fetches for editing are unaffected). Guarded the database write path too: if a record round-tripped from the list ever comes back through a save with that placeholder URL still in its `image` field, the existing stored portrait is now preserved instead of being overwritten. - **Studio's Characters tab had a redundant, non-functional action row.** The plain "Identify Characters"/"Cast Characters" tabs at the top just duplicated the labels of a richer dropdown toolbar (Identify Characters ⌄ / Cast Characters ⌄ / Cast ⌄) sitting at the bottom of the borrowed panel, and that toolbar's "Open Script Rehearser" button silently did nothing when clicked from inside Studio (its `navTo('s-rehearser')` call was swallowed by the 1.17.8 nav guard, with no error shown). The dropdown toolbar is now relocated up to replace the plain top tabs, and "Open Script Rehearser" is dropped entirely — Studio already has its own Perform & Export tab for that. ## [1.17.8] — 2026-07-18 ### Fixed - **Studio's "Cast Audiobook >" header button broke navigation.** The old cross-section workflow stepper (`_wfUpdateHeaderNav` in `utils.js`) injects its Previous/Next buttons into every section's header, including Studio's — but those buttons are hardcoded to the old Read Aloud/Library/Script Rehearsal sections and know nothing about Studio's own 4-phase system. Clicking "Next" from Studio's Source phase left the UI in a half-switched state: the step hint read "2/8 · Characters" while the tab bar still showed "Source" and the old "Already uploaded" card. The stepper's header nav is now suppressed entirely inside `#s-caststudio` — Studio's own Source/Characters/Voices/Perform & Export tabs are the only way to move between phases there. ## [1.17.7] — 2026-07-16 ### Fixed - **Perform & Export rendered a blank Stage when reached without first visiting Characters in the same session.** `_audiobook.segments` is a purely in-memory cache, only populated by actually opening the Characters phase (which triggers the draft-restore); jumping straight to Perform & Export on a fresh page load left it empty even with a fully-cast draft already sitting on the server. Now fetches the server draft directly when needed instead of requiring that detour first. ## [1.17.6] — 2026-07-16 ### Fixed - **Series voice-reuse (1.17.5) could hand a character a stranger's voice.** Confirmed live: Lysandra's own character sheet lists "Kriegerin" ("the warrior woman") as a descriptive alias, and an unrelated placeholder character in book 1 happened to be literally named "Kriegerin" — the alias-matching direction treated that coincidence as "same person" and reused the wrong voice. Narrowed to exact-name matching only; a proper name repeating identically across books is a safe signal, a descriptive epithet coinciding with someone else's literal name is not (same class of false positive already found and fixed once this session for the identity-merge logic elsewhere). One character's voice on the live book was corrected by hand after the live repro. ## [1.17.5] — 2026-07-16 ### Added - **Voice assignment now reuses the same voice across a multi-book series.** The Character Library keys each character record per book (`book::name`), so a recurring character (e.g. "Nyrilla" in episode 2 of a 3-part novel) had no automatic link back to her already-voiced record from episode 1 — auto-assign and voice design would pick/generate a fresh, differently-sounding voice every time instead of keeping the cast consistent. "Automatisch", "Design a Voice", and bulk "Auto-design voices" now all check (by name and alias, across every other book) whether this character already has a voice from a different episode first, and reuse it directly instead of assigning/designing a new one. ## [1.17.4] — 2026-07-16 ### Added - **Library production sections are now collapsible**, with a chevron toggle per book. Landing here scoped to one specific book (Studio's Voices phase, the old "Assign Voices" step) now collapses every other production and expands only the one being worked on, instead of dumping every book's whole roster on screen at once. Manual collapse/expand choices persist per book. ### Fixed - **Studio's "Cast Characters" tab had noticeably less capability than the old page** — it only offered a single "Cast from Reader" button (equivalent to "Cast all, from scratch"), missing "Continue uncasted characters", "Cast selected character roles", and "New recast" entirely. Root cause: in the old app these are two dropdown buttons ("Identify Characters ⌄" and "Cast Characters ⌄") sharing ONE footer on ONE panel — splitting them into two separate Studio tabs left the real menu only reachable from the "Identify Characters" tab, with this tab falling back to a much weaker built-in placeholder. Now points at and opens the real menu instead of duplicating a lesser version of it. - **Studio kept getting silently kicked back to the old Read Aloud section** during long-running operations (opening a large book, running "Continue casting" on a partially-cast book) — several places in reader.js/audiobook.js call `navTo('s-reader')` themselves as a "make sure the right section is showing" safety measure, including ones buried deep inside multi-second PDF parses or multi-minute LLM casting passes. Chasing and patching each one individually didn't scale (found three separate cases live). Replaced with a persistent guard: while Studio is the active section, any `navTo` call targeting a section Studio borrows from (Read Aloud, Library, Script Rehearser) is dropped as an internal reflex — unless the user just clicked that section's own sidebar entry, which is unambiguous real intent to leave and is let through. Covers every current and future call site uniformly. ## [1.17.2] — 2026-07-16 ### Fixed - **Studio's Source phase rendered unstyled/cramped** (Speed/Seed/Temperature/Native speed as plain stacked rows, Paste/Drop-document stacked instead of side-by-side). Root cause: the CSS for this panel was scoped with `#s-reader` as a required ancestor (`#s-reader .reader-tuning-row`, etc.) — accurate when the markup lived only in Read Aloud, but once Studio reparents the same DOM under `#s-caststudio`, those rules stopped matching entirely and it fell back to unstyled browser defaults. Extended the ~30 affected rules to also match under `#s-caststudio`. ### Added - **"Already uploaded" book list in Studio's Source phase** — the same Library "Books" list, so you can open a previously-imported book directly from Studio instead of only being able to paste/import fresh text. ## [1.17.1] — 2026-07-16 ### Fixed - **Stray space before the closing German guillemet «** in dialogue text extracted from PDFs (e.g. "hier. «" instead of "hier.«") — this scanned book's font renders the closing quote glyph as its own separate text item, and `readerBuildSentences` joined every extracted word with an unconditional leading space, baking the stray space right in before it. The casting LLM then faithfully preserved it, since it's instructed to reproduce the source text exactly. Fixed to skip the leading space before any word that's purely closing punctuation (`«`, `"`, `'`, `'`, `"`, `)`, `]`). Applies to newly imported/extracted PDFs — a book already extracted before this fix keeps the existing spacing unless re-imported. ## [1.17.0] — 2026-07-16 ### Added - **New "Studio" section** (Speak menu) — a single 4-phase view (Source → Characters → Voices → Perform & Export) over the same PDF-to-audiobook pipeline previously spread across 8 confusing steps split between Read Aloud and Script Rehearser. It's a shell, not a rewrite: each phase borrows the exact same DOM/logic those sections already use (PDF import, LLM speaker-attribution casting, character sheets, the Library's voice-assignment cards, and Script Rehearser's Stage editor) via runtime DOM reparenting rather than duplicating any of it — so every bug fix made to those systems all session (voice-pill alias-merge redirect, Narrator card, emotion/speaker desync, etc.) applies here unchanged. Read Aloud and Script Rehearser stay in the sidebar, fully working, unchanged, while Studio is validated — nothing was removed. - **Perform & Export phase has one "Generate full audiobook" toggle** replacing the old two-section split: on, Narrator lines get voiced and per-character "I play this" live-recording is hidden; off, Narrator stays silent (or reads via `Skip descriptions`) and each character can be marked as user-played instead of TTS'd — the exact same underlying fields (`skipDescriptions`, `narratorVoice`, `cast[x].voice==='me'`) Script Rehearser's own toolbar already exposed, just as one switch instead of two different screens. ### Fixed - **Script Rehearser's Stage view could silently render as a completely empty page for books with a large, richly-illustrated cast.** Root cause: the per-line character portrait (added earlier this session) embedded the character's full base64 image data directly into every dialogue line's HTML — a character speaking hundreds of lines re-embedded their own multi-hundred-KB portrait that many times, ballooning a real 1968-line script to hundreds of megabytes and silently failing to render. Fixed by serving portraits from a proper URL (`GET /api/characters/{id}/image`, new route) that the browser fetches and caches once, exactly like voice pictures already do — instead of inlining the raw image data. This bug existed in the already-shipped Script Rehearser too, not just the new Studio section. ## [1.15.14] — 2026-07-16 ### Changed - **Script Rehearser's Stage line rows now show the character's real portrait** on the play button, instead of a generic "?"/voice-icon placeholder for any speaker whose assigned voice had no picture of its own — same portrait already shown in Cast Audiobook, Assign Voices, and the Cast sidebar strip right next to it. (Per-line emotion tag, edit-text pencil, and personal notes were already there — this closes the one visible gap.) ## [1.15.13] — 2026-07-16 ### Fixed - **Speaker names didn't match the lines in Script Rehearser's Stage view**, even though Cast Audiobook showed the correct attribution — confirmed live on a 1958-segment script: `parseScript`'s character-name-cue regex was ASCII-only (`[A-Z...]`), so any speaker name with a German umlaut or ß (e.g. "Turmwächter", "Freischärler", "Mädchen") uppercased to a string the regex couldn't match. Those cues were silently dropped — both the speaker line and its dialogue fell through to plain narration — which desynced the per-line emotions array (built with one entry per dialogue segment) from the actually-parsed dialog lines by one for every dropped cue. Six such names in this book meant every speaker/emotion pairing after the first drop point was shifted, eventually swapping entirely unrelated characters' lines. Fixed both the ALL-CAPS name-cue regex and the "CHAR: dialogue" colon-format regex to accept any Unicode uppercase letter plus ß. ## [1.15.12] — 2026-07-16 ### Fixed - **"Next" button stuck disabled on Assign Voices (5/8), unable to reach Script Rehearser (6/8) at all.** The Script Rehearser step was only enabled once `rehState.lines.length` was populated — but that's only ever set by loading a script *into* the Rehearser, which is exactly what clicking this step does. From a fresh session neither could happen first, permanently blocking the only path in. Now also enabled once the current Audiobook has cast segments ready, matching the fallback the click handler already builds from. ## [1.15.11] — 2026-07-16 ### Fixed - **Narrator card wasn't clickable at all.** The synthetic Narrator record added in 1.15.10 was inserted into each production's card list but never into the `byId` lookup map that `_wireCharCards` uses to find each card's click target — so clicking it (voice pill, avatar, anything) silently did nothing. ## [1.15.10] — 2026-07-16 ### Fixed - **Could not give an alias-duplicate card its own voice at all** ("Kolon Tunneltreiber" — an alias-duplicate of "Kolon" — always redirected the write onto "Kolon" no matter what, blocking progress). The picker's "pick" and "Automatisch" actions now write directly to the exact record you clicked (by id) instead of running through the identity-scan that decides where writes "really" belong — that scan is right for automated casting passes avoiding duplicate creation, but wrong for an explicit, unambiguous per-card action. Use "Bibliothek an aktuelle Besetzung anpassen" afterwards to clean up duplicate rows once you've sorted out which one should stay. ### Added - **Narrator can now get a voice from Assign Voices.** The Narrator was never a real Library record (kept out of the cast library on purpose), so the only place to set its voice was the Script Rehearser's own Cast tab, in a rehearsal-local field that isn't shared elsewhere — easy to lose track of, especially now that the pipeline jumps straight to Stage. A Narrator card is now pinned first in every production's grid; picking a voice for it saves like any other character, and the Rehearser picks it up the same way it already does for the rest of the shared cast. ### Changed - **"Hear a sample" play button in the banner is now green**, matching the assigned-voice pill. ## [1.15.9] — 2026-07-16 ### Fixed - **Picking a voice sometimes silently didn't save on the card you clicked.** Root cause confirmed live: `clUpsert` treats two library records as the same character whenever one's `aliases` field lists the other's name — "Kolon"'s aliases included "Kolon Tunneltreiber" and "Kolon der Zwerg" verbatim (leftover from an earlier casting pass that never got cleaned up), so assigning a voice to the still-separate "Kolon Tunneltreiber" card silently wrote it onto the "Kolon" record instead, leaving the clicked card looking untouched with no error. The picker now detects this redirect and shows a toast naming which record the voice actually landed on, with a pointer to "Bibliothek an aktuelle Besetzung anpassen" (Cast menu) to clear out the duplicate rows. ## [1.15.8] — 2026-07-16 ### Changed - **"Hear a sample" play button moved back into the banner**, directly in front of the voice pill, instead of at the bottom of the card — it had gone missing from easy reach after the voice info moved up to the pill in 1.15.6. ## [1.15.7] — 2026-07-16 ### Fixed - **Voice pill overlapped the select checkbox** in the top-left corner of Library cards — both were anchored at the same `top:10px; left:10px`. Pill now starts after the checkbox's width instead. ## [1.15.6] — 2026-07-16 ### Fixed - **Voice picker popup (Library detail page, card/table pick-voice buttons) had a transparent background**, letting the page content underneath bleed through the list and make it hard to tell what was actually clickable. Root cause: `background:var(--card)` referenced a CSS variable that was never defined anywhere (only `--surface`/`--panel` exist) — an invalid/missing custom property falls back to the property's initial value, which for `background` is transparent. Fixed to `var(--surface)` (also fixed the same bug on `.lib-tab`). ### Changed - **Library cards now show the assigned voice (or "Keine Stimme zugewiesen") as a clickable pill at the top of the banner**, replacing the small icon-only badge — the voice is the single most important thing to check per character, so it's promoted to a glance instead of scrolling to the bottom of the card. Clicking it opens the same pick/search popup used elsewhere, now with a "Neue Stimme designen" quick action at the top for jumping straight to Voice Design. The bottom-of-card voice row now only shows the "Anhören" (hear a sample) button, since the name/empty-state text moved up to the pill. ## [1.15.5] — 2026-07-16 ### Fixed - **Stage still showed 0/0 and an empty script/character pane after the 1.15.4 fix** — `showPhase(3)` only toggles which phase `<div>` is visible; the actual script text and character sidebar are built by `buildScriptPage()`, which the direct jump-to-Stage call skipped. Now calls `buildScriptPage()` (and `highlightCurrentLine()`) before switching phase, same as the Stage sub-tab's own click handler already does. ## [1.15.4] — 2026-07-16 ### Fixed - **"Script Rehearser" step in the pipeline stepper landed on an empty Stage (0/0 lines).** Clicking the step number or the "Next" button from Assign Voices only navigated to the Rehearser section — nothing actually loaded the current cast/script into it, unlike the Rehearser's own "Rehearse" entry points which did. Now builds the Rehearser record from the live Audiobook segments (same cast and voices already assigned in Assign Voices) whenever no script is loaded yet, leaving an in-progress Stage session alone if one already exists. - **Skips straight to the Stage view instead of the Cast tab** when opening the Rehearser from the Audiobook pipeline — by that point voices were already assigned in Assign Voices, so landing on Cast again was pure redundant re-work. ## [1.15.3] — 2026-07-16 ### Changed - **Card name row now shows tier badge (Haupt/Neben) after the gender icon**, same line, instead of before the name. - **Voice-assigned badge moved to the banner's top-right corner**, left of the "change photo" camera button, instead of living inline in the name row where it landed at whatever height the vertically-centered name row happened to be. ### Removed - **SillyTavern export button removed from Library cards** (still available from the table view) — it was cluttering the compact card banner for a rarely-used action. ## [1.15.2] — 2026-07-16 ### Changed - **Library card body facts (Occupation/Archetype/Gender/Age/Lines) are now a compact single-line list** instead of a 2-column grid of boxes — each row only takes the height its own text needs, and a fact with no value is skipped entirely instead of rendering an empty box just to keep the grid aligned. Frees up meaningfully more room in an already-tight card. - **Library character cards now use a playing-card aspect ratio (5:7)** for a uniform, deck-like grid. The body flexes to fill whatever space is left below the square banner instead of relying on a hardcoded height, so the card's total size is driven purely by its own width via `aspect-ratio`. Reverts to natural (content-driven) height on mobile's single-column layout, where a 5:7 ratio at full viewport width would stretch the card absurdly tall. ## [1.15.1] — 2026-07-16 ### Changed - **Library card banner is now a 1:1 square** instead of a fixed 150px strip, so a saved portrait reads as an actual photo rather than a short crop. Its height now comes from the card's own width (already uniform across a grid row), and the card body below has a fixed height with internal scroll — together they keep every card the same total height without hardcoding one number that fought the new banner proportions. - **Removed the gender/age/lines pills from the banner overlay** — they were duplicating the Gender/Lines stat boxes already in the card body. Age now gets its own stat box down there instead, next to Gender, rather than only ever showing on the banner. ## [1.15.0] — 2026-07-16 ### Added - **"Clean up library to current roster"** (Cast menu, Cast Audiobook view). Removes saved character records that aren't in the current casting run's roster — the Character Library accumulates a record for every name any past casting run has ever produced for a book with no cleanup tied to the current cast, confirmed live: 65+ saved records for a book whose current roster is 46 names, including spelling-variant duplicates ("Globo Brohm" / "Gombo Brohm" / "Gernot Brohm") and one character split three ways ("Kolon" / "Kolon der Zwerg" / "Kolon Tunneltreiber") that a past, less-accurate pass never merged away. Shows the full list of what would be removed before deleting anything. ### Changed - **Library character cards are now a fixed size instead of growing with content.** A character with lots of aliases/occupation text used to stretch its own card — and, since grid rows size to their tallest item, every other card sharing that row — noticeably taller than a sparse one right next to it, making the grid look ragged. Overflowing banner text now clips instead of growing the card; overflowing body content scrolls internally. ## [1.14.99] — 2026-07-16 ### Fixed - **A character's real-name fields could get contaminated with a completely different character's name.** Confirmed live: "Darrag" ended up with `first_name`/`last_name` both set to "Riedmar" — a separate, unrelated cast character, not a revealed alias (Riedmar's own record was blank at the time, ruling out a merge-logic bug — the model itself confused whose name belonged where, likely from the two characters appearing together in the same passage). Now: if a generated sheet's first/last/full name exactly matches a DIFFERENT known character's name, that field is dropped instead of trusted. Manually corrected the live Darrag record. - **"Cast from Reader" (and the equivalent Rehearser/selective-cast entry points) could silently land back on the plain Source view instead of starting the cast**, showing an unrelated "resuming at sentence…" toast. Root cause not fully isolated (a race with the reader's own un-awaited async setup that runs every time its section becomes visible), but the practical fix doesn't need to be: the intended view is now re-asserted a moment later, winning the race regardless of which async step caused it. ## [1.14.98] — 2026-07-16 ### Added - **Pencil icon to edit a character sheet's field values directly on the live casting card**, next to the color-swatch button in the header. Each field saves on its own debounce timer, so editing two fields in quick succession can't cancel one another's pending save. Known limit: a field that's currently empty isn't rendered at all here, so adding a brand-new value to an empty field still needs the full Library profile page — this covers correcting a value that's already shown. ## [1.14.97] — 2026-07-16 ### Added - **"Design Voice" and "Generate Profile Image" action buttons directly on the Voice Design / Character Image prompt boxes.** Copy/Regenerate only ever acted on the prompt text itself — actually using it meant copying it out and pasting it somewhere else by hand. One click now does that directly with whatever prompt is already in the box. ### Changed - **Removed the ST / TTS / Bild indicator columns from the Cast table.** These only ever showed whether a prompt had been generated, which was redundant with information already visible elsewhere in the same row (the actual profile picture thumbnail, and the assigned voice in the Stimme column). ### Fixed - **"Generieren" (voice) always built a fresh, cruder prompt from raw sheet fields, ignoring the properly-crafted Voice Design Prompt already generated and sitting right there on the card.** Now uses the saved prompt when one exists, only falling back to the ad-hoc builder when nothing's been generated yet. ## [1.14.96] — 2026-07-16 ### Fixed - **The voice picker popup ("Auswahl") could render floating several rows away from the button that opened it**, in the Cast table view specifically. Root cause: the popup was appended as a direct child of the trigger element with `position:absolute` — fine for a card-grid `<div>`, but the table view's trigger is a `<tr>`, and a `<div>` can't legally live inside a table row. Browsers silently relocate invalid table content out of the table structure, landing the popup at an unrelated position. Now appended to `<body>` as a `fixed`-position popup anchored via `getBoundingClientRect`, the same proven pattern already used for the alias and footer-menu popups elsewhere in this app — works identically in both the card and table views. - **The live casting card's avatar only opened a bare file picker** — no way to paste an image URL or regenerate one from the character's own image prompt without leaving the view. Now opens the same full lightbox (disk / URL / AI-regenerate) used everywhere else. ## [1.14.95] — 2026-07-16 ### Added - **Library character cards now use an existing portrait as the whole card banner** (full-bleed background with a bottom-fade for legible text), instead of squeezing it into a small 96px circle — a real photo deserves more than icon size. The avatar circle shrinks to a small "change photo" corner button in that case, so re-uploading still works the same way. - **A voice-assigned badge sits right next to each character's name** in the card grid — filled green speaker icon when a voice is assigned, muted outline when not. Previously this was only readable as plain text at the very bottom of a (possibly long) card, so scanning a whole cast for "who still needs a voice" meant reading every card in full. ### Fixed - **The live passage-by-passage casting view (Character sheets step) never showed an already-saved profile picture** — neither the main card's avatar nor the "Characters found" sidebar list. Root cause: the seeding step that loads each character's seed sheet from their saved library record dropped the `image` field entirely, so even a character with a real portrait always fell back to the plain letter placeholder in this view. Now carried through to both the card and the sidebar. - **That same card's avatar was capped at a small square even with a real photo** — now stretches to fill the full height of the colored header banner instead of sitting shrunk in the corner. ## [1.14.94] — 2026-07-16 ### Added - **Play button on every cast character with a voice assigned**, in both the Library's card and table views — hear a quick sample line in that voice without opening the full profile or leaving the page. Mirrors the Rehearser's own "Hear a line" button; toggles to a stop icon while playing. Since the Library only stores a character sheet (not actual script lines), the sample is a short generic greeting, in German or English depending on the character's own sheet text. - **"Continue uncasted characters" now skips characters with fewer than 10 lines by default** instead of endlessly re-trying them. Confirmed live: a full-roster recast filled in the one prominent character and left every character under ~20 lines completely blank — a character with only a handful of lines usually just doesn't have enough text for the LLM to say anything real about them, so retrying burns a call that almost always comes back empty anyway. The toast now reports how many were skipped this way. ## [1.14.93] — 2026-07-15 ### Fixed - **"Continue uncasted characters" could re-split an already-merged character back into two records forever.** It checked whether a roster name already had a library record by exact name only, never checking aliases — so once a character-sheet pass proved two roster names (e.g. a pre-reveal alias and an already-established dialogue-attribution name) were the same person and merged them under one canonical name, the OTHER name still looked like "never cast at all" on every future run and got endlessly re-queued as its own separate target, recreating the split every time. Now also matches against every existing record's recorded aliases before deciding a roster name is uncasted. - **A second copy of the shared-debounce-timer bug**, in the casting view's own inline character-detail panel (fixed once already this session in the Library's detail page, missed this second copy): editing two fields within 900ms could silently drop the first edit. Same fix — one timer per field instead of one shared timer. ## [1.14.92] — 2026-07-15 ### Fixed - **Opening a character's full profile from the Read Aloud recast-results grid rendered it squeezed into a sliver with the sidebar overflowing to the side.** That results grid (`.cs-list`) is a CSS grid with `minmax(300px, 1fr)` card columns; the detail page's own two-column layout (main content + 240px character-navigation sidebar) got inserted as a single grid item, confining it to one column's width instead of the full row. The detail page now explicitly spans every column when it renders — a no-op in the Library's own plain container, so this only changes the one place that was actually broken. ## [1.14.91] — 2026-07-15 ### Fixed - **Regression from earlier today: a character referred to mostly by an alias could end up completely blank after a full recast, even with 100+ lines of dialogue.** The "output nothing if this passage is about someone else" instruction added to prevent cross-contamination (v1.14.89) was too broad — it also fired whenever a passage used a target's alias/nickname/role instead of their literal roster name (e.g. "der Schmied" instead of "Darrag"), even though the character's own already-recorded sheet said that alias belonged to them. Confirmed live: a synthetic "der Schmied" passage with Darrag's alias already on record returned an empty sheet before the fix, a fully-detailed one after. Caught this by directly comparing the API's raw output before/after against known real-book passages (Darrag/"Fremder" both had zero recorded sources despite 100+ lines each) rather than trusting the instruction wording alone. The model now explicitly checks the passage's own context AND the existing-sheets summary for an alias match before ever deciding a passage is unrelated. ## [1.14.90] — 2026-07-14 ### Fixed - **"Verify all characters" could duplicate a paragraph into the cast, with a stray quote mark on the second copy.** Confirmed live: the same narration paragraph appeared twice, separated by an unrelated dialogue exchange — the source PDF only had it once. Root cause: each recast group pads its request with ±5/+4 segments of surrounding context so the LLM has continuity across the group's actual targets, and adjacent groups' context windows can overlap by that much; splicing a group's multi-segment replacement back in can then reintroduce text a neighbouring group's overlapping window already restated correctly a few segments earlier. Added a dedup pass after the verification run that removes a near-duplicate segment found within a nearby window (normalizing away stray leading/trailing quote marks before comparing, since the duplicate copy often carries one) and reports how many it removed. ## [1.14.89] — 2026-07-14 ### Fixed - **A field's source citation badge only ever showed the FIRST supporting quote, silently hiding any others.** If "Relationships" was backed by 3 separate passages, only one citation mark ever appeared next to it — the rest existed in the saved data but had no way to reach them from that field. Now shows one small numbered mark per matching source, each numbered to match its position in the full "Sources / Evidence" list at the bottom of the card. - **Clicking a source citation only jumped to the top of the page, not the actual sentence.** Now also runs the Reader's own search (highlighting + auto-scroll) for the quoted text right after the page jump, so the exact line is visible in context instead of "somewhere on this page, go find it." - **Character-sheet generation could cross-contaminate two different characters' profiles.** When a per-character "evidence window" pass (used by "Continue uncasted characters" and manual re-cast) landed on a passage that was actually about a DIFFERENT character than the one being processed, the model had no valid way to say so — it wasn't allowed to invent a new profile, so it forced the unrelated character's information into the wrong profile's fields (caught live: a passage about "Marcian" got written into "Fremder"'s Notes field, along with the model's own visible confusion about what to do). The prompt now explicitly allows outputting nothing for a passage that isn't actually about any of the current targets. ## [1.14.88] — 2026-07-14 ### Added - **3rd casting quality pass: "Check voice consistency"** (Identify Characters menu, Read Aloud → Cast Audiobook). Unlike the first two passes — which both re-read the source text chunk by chunk, since a whole book is far bigger than any context window — this one works purely over already-attributed lines already in memory: for each character with 4+ lines, it gathers every line credited to them from anywhere in the book (evenly sampled up to 60 for very talkative characters, so their arc late in the book is represented too, not just their first appearances) and sends that whole bundle to the LLM in one call, asking it to flag any line that doesn't match the voice established by the REST of that character's own lines. Catches a misattribution that reads fine in isolation but doesn't fit the character once you see their whole body of dialogue together — something no single passage-sized window could ever expose. Only ever reassigns to an already-known character name or 'Unknown', never invents a new one. New server route: `POST /api/audiobook-consistency-check`. Caught live during testing: the model sometimes echoed its own sequential count instead of the real line index — added a quoted-excerpt field to the response and a text-match fallback so a flagged line can't silently misapply to the wrong segment. ## [1.14.87] — 2026-07-14 ### Fixed - **"Failed to fetch" during character-sheet generation wasn't retried at all.** The retry-with-backoff added earlier this session only matched 429/rate-limit errors — a plain dropped connection, DNS blip, or the backend restarting mid-run failed that passage permanently on the first hit, silently leaving it unprocessed for that run. Now covered by the same retry logic (shorter backoff than rate limits, since network blips usually clear faster). ## [1.14.86] — 2026-07-14 ### Added - **"Cast selected character roles" now shows portraits and line counts, and is searchable/sortable.** Replaced the bare alphabetical checkbox list with the same avatar + name + line-count row style as the "Characters found" sidebar in the live casting view, plus a search box and a Lines/A–Z sort — matching how you already pick characters everywhere else in the app. - **"Verify all characters" (2nd quality run) is now explicitly prioritized.** It checks, in order: (1) every 'Unknown' line first — an unattributed line is worse than a misattributed one, (2) runs of 2+ consecutive segments assigned to the same speaker (narrator or character) — the most common place a hidden speaker-change or unquoted dialogue line gets swallowed into a narration/dialogue streak, (3) a general plausibility check on everything else. Previously all of this was checked in one unordered pass. ### Fixed - **Stray quote marks were landing on the wrong segment instead of just being dropped.** German uses » to open and « to close a quote (the reverse of French) — a dialogue turn's closing « sometimes ended up glued to the FRONT of the following narration segment instead of the end of the dialogue itself (and the mirror case for opening »), including cases where the entire "segment" was nothing but the stray mark. Added a mechanical post-cast fix that moves each mark back to where it belongs, plus a new explicit prompt rule telling the casting LLM the same thing so it happens less often in the first place. ## [1.14.85] — 2026-07-14 ### Fixed - **"Verify all characters" (the second-pass quality check) was missing the post-quote inquit-tag rule.** The first-pass casting prompt has an explicit rule for lines like `"Ich bin dagegen!" ... entgegnete Oberst von Blautann.` (the dialogue comes before the name) — but the verification pass's rule list only carried over the colon rule ("X sagte:" → the following line is X's), not this one, so a Narrator-mistagged line immediately followed by an inquit tag wasn't reliably caught. Added the same "Nachgestellte Zuordnung" and action-beat rules the first pass already had. ## [1.14.84] — 2026-07-14 ### Added - **Workflow navigation now says what to actually do.** The Previous/Next buttons on every section header used to just say "Previous"/"Next" with no clue what either led to or when a step was "done" — they now show the target step's name, and a guidance bar under the header explains what this step is for and when it's safe to move on or go back to fix something. - **Adjacent same-speaker segments get merged after casting.** Two or more consecutive segments assigned to the exact same speaker (narrator or a character) with no page break between them are either an attribution mistake or one continuous block chopped into noise by the per-passage LLM pass — both are now merged into one segment automatically, book-wide (not just within one passage), instead of leaving a wall of one-line cards to review by hand. ### Fixed - **A character's voice reassignment could silently fail to save.** `clUpsert`'s persisted-record builder computed `voice: prev.voice || sheet.voice`, so once a character had ANY voice, every later reassignment (via the picker, "Auto assign", or "Design voice") updated the in-memory view and showed a success toast, but the database kept the OLD voice forever — the wrong voice then got used for every subsequent audio generation, with nothing anywhere indicating the change hadn't actually stuck. Precedence flipped so an explicit new value always wins. - **Unrelated characters could get silently merged into one profile.** `CL_IDENTITY_FIELDS` (the persisted-library identity-match list) still included `title`/other purely-descriptive fields, reintroducing a bug already fixed in the in-session merge logic (`CS_IDENTITY_FIELDS`) — two different characters sharing a job title or a reused epithet ("the Executioner") could have their sheets, backstory, and voice blended into a single record. Brought back in sync with the already-fixed list. - **A failed TTS segment used to vanish from the exported/merged audiobook with zero warning.** Both Read Aloud's page/sentence export and Script Rehearser's audiobook export caught synthesis errors per-segment and just moved on, so a mid-book backend blip produced a file "successfully" downloaded with paragraphs quietly missing — discoverable only by listening carefully. Both now count failures and refuse to export until they're fixed/retried, instead of silently shipping a gapped file. - **Generating character images could freeze the entire app for every user, for minutes.** Every image-gen provider (ComfyUI, OpenAI, Google, OpenRouter) made blocking HTTP calls directly on the request handler with no `asyncio.to_thread` wrapper — on this single-worker server, ComfyUI's own polling loop (up to 5 minutes per image) blocked the whole event loop, so a bulk "auto-generate images" run across a cast could make the entire site unresponsive for everyone, for as long as it ran, with no distinguishing symptom. Moved onto a worker thread like every other blocking call in this file already was. - **Editing two character-profile fields in quick succession could silently drop the first edit.** The detail page's autosave used one shared debounce timer for every field — switching from one field to another within 900ms cancelled the first field's still-pending save with nothing else to replace it, so that edit was never written to the record or persisted, with no error shown. Each field now gets its own timer. - **Rehearser's "Synthesize All"/"Re-synthesize stale" could get stuck locked forever.** Same failure shape as the character-sheet generation-lock bug fixed earlier: an uncaught error partway through the loop (e.g. the current line's character got deleted mid-run) left the running flag stuck `true`, silently no-op'ing every future click with no error shown. Both now run inside try/finally and skip a line whose cast entry has vanished instead of throwing. ## [1.14.83] — 2026-07-14 ### Fixed - **Character-sheet recasts could silently stop saving forever, in one tab, with no error shown.** The generation lock (`_cs.running`) was only guaranteed to clear inside a `try/finally` that started *after* the progress overlay was created — if that setup step (or anything else run once per call, before the passage loop) ever threw, the lock stayed `true` permanently. Every later recast in that same browser tab then hit the very first guard (`if (_cs.running) return null`) and did nothing at all: no toast, no save, no visible error — exactly indistinguishable from "recasting isn't working." Confirmed live: hundreds of `/api/character-sheets/stream` calls succeeded over the last two days with zero corresponding `PUT /api/characters` saves. The whole function body is now wrapped so the lock always releases and any crash surfaces as a toast instead of vanishing. A page reload already clears the flag in the meantime, since it's just in-memory state — no data was lost, but nothing new was saved until now. ## [1.14.82] — 2026-07-12 ### Added - **API-key gate for external callers** (Phase 4 of the roadmap) — off by default, since this is a live app and I couldn't test the same-origin detection against real browser traffic. Turn on "Require this key for external callers" in Settings → API Keys → External API Access once confirmed safe; the app's own UI is unaffected either way (same-origin, never needs the key). Also discovered the REST API was already comprehensive (118 documented routes, auto-generated docs live at `/docs`) — this phase was mostly already done. - **MCP tool set expanded** (Phase 5) — from 4 tools (speak/transcribe/list_captures/list_profiles, discovered already existing at `/mcp`) to 9: added `list_books`, `list_characters`, `get_character`, `update_character`, `list_rehearsals`. Note: full end-to-end "cast an entire book via one tool call" isn't included — that orchestration (chunking, streaming, progressive merge) currently only exists client-side in JS, not as a backend-callable operation; porting it is a larger follow-up, not silently skipped. ## [1.14.81] — 2026-07-12 ### Added - **i18n expanded from German-only to 7 languages** (Phase 3 of the roadmap: French, Spanish, Italian, Portuguese, Dutch, Polish, plus the existing German) — the translation infrastructure (`window.t`, `applyI18n`, the language picker in Settings → General) already existed and worked, it just only had one language's dictionary filled in. Same key set (~100 UI-chrome strings: nav, section titles/subtitles, common buttons/labels) translated into all six new languages. LLM-generated content (character sheets, prompts) is unaffected — stays in the source book's language, as scoped. ## [1.14.80] — 2026-07-12 ### Changed - **SillyTavern + Concept Art prompt boxes no longer silently disappear during an active recast** — the live character-sheet card hid all four prompt boxes until their field had content, which made sense for Voice Design/Image (filled in live, passage by passage) but was confusing for SillyTavern/Concept Art, since those only ever run as a background pass after the whole recast finishes — they looked like they'd vanished rather than "not generated yet." Those two now always show, with a note explaining they generate after casting finishes while a run is active. ## [1.14.79] — 2026-07-12 ### Fixed - **Character-sheet generation prompt hardcoded real character names from the user's actual book ("Zerwas", "Henker", "Vampir") as the identity-merging example**, in both the client-side and server-side copies of the prompt. Beyond not generalizing to any other book, this risked the LLM getting confused by its own instructions calling out a name it was actively reading dialogue for — plausibly explaining why some major characters (Zerwas: 146+ lines) ended up with a completely empty profile despite having plenty of source material. Replaced with clearly generic placeholder names ('Marcus', 'the Executioner', 'Bloodfang') in both copies. No custom prompt is currently saved, so this takes effect on the very next run with no reset needed. ## [1.14.78] — 2026-07-12 ### Added - **Focus trap + focus restore for every modal in the app** (Phase 2 of the accessibility roadmap) — the ~11 places that build a `.audiobook-overlay` dialog (confirmDialog, the avatar lightbox, csShow, voice pickers, etc.) never managed keyboard focus individually: Tab could escape a modal into the page behind it, and closing one never returned focus to whatever opened it. Fixed centrally via the existing DOM-mutation observer instead of touching all 11 call sites — Tab/Shift+Tab now cycles within the topmost open modal, and closing it restores focus to the trigger element. ## [1.14.77] — 2026-07-12 ### Added - **"Continue uncasted characters" in the Cast Characters menu** — a new option alongside "Cast all character roles" and "New recast (discard & rebuild all)" that only generates profiles for characters with zero detail yet (never cast, or only ever picked up as a bare name), skipping everyone already fully cast. Uses the same targeted evidence-window scan as "Cast selected character roles," just with the incomplete list built automatically instead of picked by hand. ## [1.14.76] — 2026-07-12 ### Fixed - **Character sheet generation permanently dropped a passage's data on a rate-limit (429) error** — a burst of requests hitting the LLM provider's per-minute cap (observed live: OpenAI 429 on passage 4 of 149) just logged the failure and moved on, silently leaving that section of the book with no character detail. Rate-limit errors now retry with backoff (up to 4 attempts, growing delay) before giving up; other error types still fail fast as before. - **Avatar lightbox and Rehearser cast card "More options" grid could overflow on narrow phones** — the lightbox's action column had a 340px minimum width and the cast card's Language/Gender/Tags row stayed 3 columns regardless of viewport; both now collapse properly on mobile. ## [1.14.75] — 2026-07-12 ### Fixed - **Script Rehearser Stage view was unusable on phone-width screens** — the character sidebar was a fixed 220px column sitting next to the script page with no responsive handling at all, leaving almost no room for the actual script on a phone. It now stacks above the page and collapses full-width instead, same collapse toggle as desktop. ### Investigated (Phase 1 of the mobile/accessibility/i18n/API roadmap) - Spot-checked Library (Cast grid + table) and Read Aloud (Reader + Casting) for the same class of gap — both already handle narrow viewports reasonably well (auto-fill grids that naturally go single-column, flex-wrap toolbars, horizontally-scrollable tables) and didn't need immediate fixes. Broader page-by-page audit still pending for Phase 1 completion. ## [1.14.74] — 2026-07-12 ### Added - **Script Rehearser Stage sidebar now has search, sort, and real character portraits** — matching what the Cast tab already gained, instead of plain colored-letter dots. Names show in the character's cast color when a real portrait is available. Search/sort now only re-render the sidebar strip, not the whole (potentially 1000+ line) script page. ## [1.14.73] — 2026-07-12 ### Fixed - **"Open Script Rehearser" could load stale/wrong cast data** — it previously saved the current cast to IndexedDB and then re-fetched it by id before loading, which could hand back an outdated record (read-after-write race, or a stale id from an earlier pass). It now builds the record from the current in-memory segments and loads that directly — no re-fetch, no possible staleness — while still persisting to the DB in the background for the "Cast again" round trip. - **Casting could leave a lone orphaned »/« character as its own narrator segment** — the casting prompt instructed the LLM to strip quote marks out of dialogue text entirely, and when it didn't fully comply, a stray quote mark landed as a standalone segment right before the actual dialogue. Quote marks are now kept attached to the dialogue text instead of being stripped (matching how they read in the source), which removes the failure mode rather than just re-emphasizing the instruction. Already-cast books need a recast to pick this up — existing segments aren't touched automatically. ## [1.14.72] — 2026-07-12 ### Changed - **Script Rehearser cast cards now literally reuse the Library's own card** (`_charCardHtml`) for every speaker that matches a real character record — same colorful portrait/tier/occupation/alignment design as Library → Cast, not a look-alike. Clicking a card opens the same full profile page (voice/language/tags/soul editing lives there now); "back" returns to the cast list. Only "I play this", ignore/hide/delete, "Hear a line", and emotion chips remain on the card itself. Narrator and unmatched/"UNKNOWN" speakers (no Library record to reuse) keep the simpler fallback card with its own inline voice picker, since there's no profile page for them to point at. ## [1.14.71] — 2026-07-12 ### Changed - **Script Rehearser cast cards decluttered** — Language/Gender/Tags, the online voice-match panel, speaking-style prompt, and Character soul are now collapsed behind a single "More options" toggle instead of always showing five-plus form fields per card. Only portrait, name, voice, play button, and emotion chips show by default, closer to the Library's own card look. Kept as Rehearser's own card grid rather than embedding the Library table directly — this tab has to handle the Narrator and unmatched/UNKNOWN speakers, which the Library's book-scoped character table doesn't model at all. ## [1.14.70] — 2026-07-12 ### Added - **Script Rehearser cast cards now show a real portrait, tier badge, and occupation/archetype** — cross-referenced from the Character Library by matching the script's book/title, same visual language as the Library's own cast cards, instead of always falling back to a colored-initial avatar. - **Emotion chips on each cast card** — every distinct speaking-tone tag used across that character's lines in the script (set via the existing per-line emotion picker) now shows as a small chip row, giving an at-a-glance sense of their emotional range before you even open the script. ## [1.14.69] — 2026-07-11 ### Fixed - **Bulk image generation produced near-identical generic portraits for sparse background characters** — a character with no physical/clothing/archetype detail (typical for 2-3-line crowd extras like "Ruf aus der Menge" or "Zwei Gestalten") fell back to a prompt with nothing character-specific in it ("an original character named X, ambiguous expression"), close enough across dozens of characters that several providers returned visibly identical results. Bulk generation now skips characters below a minimum-detail threshold instead of spending a generation on a near-blank prompt, leaving the neutral initial-letter placeholder instead. ## [1.14.68] — 2026-07-11 ### Added - **Character profile pictures now sync to their assigned library voice's own picture** — the Voices Library already had full picture upload/display support (`/api/voice/picture`), it just never had anything feeding it from the character side, so voices only ever showed generic gender/type icons. Syncs whenever a character's image changes (upload, URL, AI-regenerate, bulk generate) if they already have a voice assigned, and whenever a voice gets (re)assigned (manually, auto-assign, or auto-design) if they already have an image. Best-effort — never blocks or shows its own errors, since it's a convenience mirror of the character's real picture, not the primary action. ## [1.14.67] — 2026-07-11 ### Changed - **Avatar lightbox is much bigger** — modal width 680px → 1180px, preview image 200px → 440px, larger text/inputs throughout. The first pass was sized like a small popup instead of something meant to actually judge image quality at a glance. ## [1.14.66] — 2026-07-11 ### Added - **Avatar lightbox — click any character's profile picture in the Library grid/table to open a bigger preview with three ways to change it**: upload from disk, paste an image URL (downloaded server-side to sidestep CORS, since most image hosts don't allow direct browser fetches), or regenerate with AI using an editable prompt (pre-filled with the character's current Image Prompt, with a per-run engine picker). Replaces the previous behavior where clicking an avatar jumped straight into a file picker with no way to preview, use a URL, or tweak the prompt before generating. ## [1.14.65] — 2026-07-11 ### Fixed - **ComfyUI generation crashed with `NameError: name 'copy' is not defined`** — a leftover from moving the route's ad-hoc imports to the top of the file; `copy` was never actually added there. Caught by testing the endpoint directly before relying on it. ### Configured - **Local ComfyUI wired to an actual working text-to-image workflow** — a minimal 4-step Flux Schnell pipeline (checkpoint/CLIP/VAE already present on disk), verified end-to-end: submitted, polled to completion (~30s), and the resulting image confirmed as a real, on-quality character portrait. Chosen over the much larger "Consistent Character Creator" workflow specifically because it's simple enough to trust without ComfyUI's own UI export step. ## [1.14.64] — 2026-07-11 ### Fixed - **Bulk "Auto-generate images" gave no visible progress until the entire batch finished** — avatars only refreshed on the final grid re-render, so a long run (especially with a slow local ComfyUI workflow) looked frozen except for the toolbar's "N / total" counter. Each avatar now updates in place the moment its own image comes back, in both Cards and Table view. ## [1.14.63] — 2026-07-11 ### Added - **Per-run image-generation engine picker in the Library toolbar** — a dropdown next to "Auto-generate images" lets you pick OpenAI/Google/OpenRouter/Pollinations.ai/ComfyUI for just that run, overriding the Active Provider in Settings instead of having to go change it first. Defaults to whatever's currently active. ### Changed - **Character avatar thumbnails in the table view are bigger** (26px → 48px) — the previous size was too small to actually judge a generated image's quality at a glance. ## [1.14.62] — 2026-07-11 ### Fixed - **Pollinations.ai now retries automatically on "queue full" errors** — the free shared queue rejects requests under load instead of queueing them; a first live test failed this way and succeeded on manual retry, so that retry (up to 3 attempts, 4s apart) is now automatic. ### Added - **Pollinations.ai card in Settings → Engines → Image Generation → Cloud APIs**, alongside OpenAI/Google, documenting that it needs no key/setup — just select it as the Active Provider. Removed the stale "ComfyUI planned as a follow-up" note now that it's actually implemented. ## [1.14.61] — 2026-07-11 ### Added - **Local ComfyUI as an image-generation provider** (Settings → Engines → Image Generation) — paste an API-format workflow export (Workflow → Export (API Format) in ComfyUI's own UI, not a regular saved workflow), point it at your prompt node/field and output node, and character portraits/concept sheets generate on your own GPU instead of a paid API. Includes "Test connection" (reachability check) and "Test generate" (runs a real prompt through it and shows the result inline) so you can validate the setup without going through a full bulk-generate run. Deliberately does not attempt to auto-convert ComfyUI's editor-format graphs — workflows using virtual-routing addons (e.g. rgthree Get/Set nodes) don't show up as real graph edges and a naive converter could silently mis-wire them. - **Pollinations.ai as a free, no-API-key image-generation provider** — a stopgap for when the paid providers' billing isn't sorted yet; third-party public service, no SLA or account needed. ## [1.14.60] — 2026-07-11 ### Added - **Search box in the status bar's LLM model fly-up** — OpenRouter alone lists 50+ models, making the plain scrollable list slow to use; typing now live-filters it. Applies to any fly-up with more than 8 entries (STT/TTS backend pickers included, though those rarely have that many). - **OpenRouter as a third image-generation provider** (Settings → Engines → Image Generation), alongside OpenAI and Google — routes through the same chat/completions endpoint and API key as the OpenRouter LLM card (no separate key needed), for image-capable models like `google/gemini-2.5-flash-image-preview`. ## [1.14.59] — 2026-07-11 ### Added - **Connect and "Use as LLM" buttons on the Anthropic card too**, for consistency with the other five Cloud API cards. Connect now probes correctly for Anthropic's actual auth scheme (`x-api-key` + `anthropic-version` header) instead of the generic Bearer-token check the other providers use, which would have falsely shown "unreachable" even for a valid key. "Use as LLM" now warns before applying it, since this app's LLM calls are OpenAI-format (`/chat/completions`) and Anthropic's Messages API isn't compatible — the button works, but generation will likely fail until real Anthropic support is added. ## [1.14.58] — 2026-07-11 ### Added - **Connect and "Use as LLM" buttons on the Cloud API cards** (Groq, OpenRouter, Google Gemini, Mistral, OpenAI) — previously only the local Docker engine cards further up the page had these; the cloud cards only let you store a key with no way to test it or make it the active LLM. Connect probes the endpoint with the entered key and shows a persistent Connected state; "Use as LLM" applies the endpoint, key, and a sensible default model to Settings in one click, matching the local cards' behavior. Not added to the Anthropic card since it isn't OpenAI-compatible and would silently break. ## [1.14.57] — 2026-07-11 ### Added - **OpenAI and Anthropic cards in Settings → Engines → Language Models → Cloud APIs**, alongside Groq/OpenRouter/Google Gemini/Mistral — same API key storage pattern as the existing cards. Anthropic is flagged as not OpenAI-compatible (needs the Messages API, not `/chat/completions`), since every other card here — including this app's own LLM calls — assumes an OpenAI-compatible `/chat/completions` endpoint. ## [1.14.56] — 2026-07-11 ### Changed - **The image-generation Model field is now a dropdown of known models per provider, not a blank text box** — there was nothing to "select" because it was free text with only a greyed-out placeholder, so you had to already know a valid model ID to use it at all. Now shows curated options for OpenAI (gpt-image-1, dall-e-3, dall-e-2) and Google (gemini-2.5-flash-image and its preview variant, imagen-4.0 and imagen-3.0), plus a "Custom…" option that reveals the free-text field for anything newer than this list. Switching provider resets the model to that provider's default instead of silently keeping an incompatible model ID from the other one. ## [1.14.55] — 2026-07-11 ### Fixed - **Clicking a character card in the fresh-recast results grid (Read Aloud → Character sheets) did nothing** — the detail page always rendered into `#lib-chars-list`, a container that only exists on the Library page. It now accepts an explicit target container and back-navigation callback, so opening a profile from any card grid in the app (not just the Library) works and "back" returns to wherever you actually came from. ### Added - **SillyTavern and Concept Art prompts are now generated automatically as part of a recast** — previously only the Voice Design and Image prompts were filled in during the passage-by-passage pass; the other two needed a manual "Generate" click per character. They now run as a background pass right after a recast finishes (its own progress/result toast), using the character's complete profile in one LLM call per prompt. - **"Clone" as a fourth voice option on the character profile page**, alongside Pick/Auto/Online/Design — opens Clone a Voice with the character's name pre-filled, ready for a mic take, file, or YouTube URL (cloning needs a real audio source only the user can supply, so this just gets them to the right screen instead of automating a step with no sane default). - **Explicit Upload / Search online / Generate buttons under the profile picture**, instead of only an upload-on-click avatar plus one AI-generate button — "Search online" opens an image search in a new tab (built from the character's name, book, and archetype) for the user to browse and save an image themselves. ## [1.14.54] — 2026-07-11 ### Fixed - **The "Delete characters?" confirmation dialog's red confirm button was invisible** — `.btn-danger` and a few other rules (error toasts, danger menu items) styled themselves with `var(--error)`, but no `--error` CSS variable was ever defined (only `--red`), so the button rendered with no fill at all instead of solid red — it looked unclickable rather than like a button. `--error` is now defined alongside `--red` in both the light and dark themes. ## [1.14.53] — 2026-07-11 ### Fixed - **Bulk actions in the Character Library only reported a fail count, never the actual error** — "Generating images finished for 0 characters (79 failed)" gave no way to tell why without opening DevTools. The toast now includes the actual error message (e.g. an API quota/billing limit), and a bulk run stops early after 3 characters in a row fail with the identical error instead of grinding through the rest of a selection hitting the same systemic wall. ## [1.14.52] — 2026-07-11 ### Changed - **Bulk voice-design sample text now falls back to a real quote from the book before the generic filler sentence** — the fallback only kicked in when the character had no attributed dialogue line in the currently-loaded audiobook cast, which is the common case when designing in bulk straight from the Library (outside that book's cast context). It now tries the character's own sourced quotes next, preferring one that reads like something they actually said, before resorting to the neutral placeholder line. ## [1.14.51] — 2026-07-11 ### Fixed - **Bulk-designed character voices showed up as "Clone" instead of "Design" in the voice library** — the headless auto-design flow saved a reference sample the same way manual cloning does, but never tagged the result with the `origin: designed` marker the UI's Clone/Design classification checks for. All 75 already-saved auto-designed voices were retagged in place. ### Added - **Bulk-designed voices are now grouped and tagged by the book they were designed for** — same `group`/`tag` metadata the Script Rehearser's own voice-design flow already sets, so a book's character voices cluster together instead of scattering through the flat voice list. ## [1.14.50] — 2026-07-11 ### Added - **Select-all checkbox in the Character Library table header** — checking it selects/deselects every row directly from the column it lives in, in addition to the existing toolbar "Select all" button; the two stay in sync (including an indeterminate state when only some rows are checked). ## [1.14.49] — 2026-07-11 ### Fixed - **Every page went blank right after the 1.14.48 deploy** — the new header Previous/Next injection (`_wfUpdateHeaderNav`) was called with a `curIdx` variable that only existed inside the stepper-rendering loop, throwing a `ReferenceError` on every single `refreshWorkflowCrumbs()` call (which runs on essentially every page render). The exception aborted whatever page-render code ran after it in the same call stack, leaving pages showing only their header. `curIdx` is now computed once, before the loop, in scope for both the stepper and the header-nav update. ## [1.14.48] — 2026-07-11 ### Added - **"Select all" and "Delete selected" in the Character Library grid** — select-all toggles every checkbox in a production at once; delete removes the checked records outright (there was previously no delete option at all in this grid — needed for clearing stale/corrupted entries before a fresh recast). - **Previous/Next workflow buttons on every page's section header**, not just the stepper widget further down — same navigation, one click away without scrolling. ### Changed - **The stepper's back-to-previous-step button moved from the title row down to the numbered-steps row**, sitting with the rest of the step navigation instead of next to the book title. ## [1.14.47] — 2026-07-10 ### Added - **"Auto-design voices (selected)" and "Auto-generate images (selected)" bulk actions in the Character Library grid** — auto-design is fully headless (generates and saves a brand-new voice per checked character from their Voice Design Prompt, no navigation to the Design screen needed); auto-generate calls the configured image provider per character and saves the result as their profile picture. Both run sequentially over the selection with live progress. ## [1.14.46] — 2026-07-10 ### Fixed - **Character gender was silently wiped to blank for every non-English answer** — the character-sheet cleanup validated `gender` against only the literal strings "male"/"female"/"nonbinary"; a German book naturally gets German answers ("weiblich"/"männlich"), which failed that check and got discarded instead of normalized. Now maps common English and German variants to the canonical value. ## [1.14.45] — 2026-07-10 ### Fixed - **The page could hang/appear blank for many seconds on load** — a `MutationObserver` re-ran a full-document accessibility scan (several `document.querySelectorAll` passes over the entire page) on every single `class` attribute change anywhere in the app, instead of just the changed element. This session's larger dynamic grids/cards made class mutations frequent enough to compound into multi-second main-thread blocks (visible as `requestAnimationFrame`/`setTimeout` violations). The attribute-change handler now only does the cheap, targeted nav-active-state sync it actually needs, not a full re-scan. ## [1.14.44] — 2026-07-10 ### Changed - **Character Image Prompt now asks for a full reference/turnaround sheet instead of a single portrait** — full-body front view, side/back turnaround, a small expression sheet, a color palette swatch, and labeled prop/clothing callouts in one composite image, matching production concept-art conventions. Applies to both the LLM-generated prompt and the client-side fallback used before that's been generated. ## [1.14.43] — 2026-07-10 ### Added - **New "Assign Voices" workflow step between Cast and Script Rehearser** — jumps straight to the current book's block in the Character Library grid (voice pick/auto/online/generate, AI image generation, and per-character sheet regeneration already live there) instead of landing on the whole cross-book library unfocused. - **AI-generated character profile pictures** — a new "Image Generation" page under Settings > Engines lets you configure OpenAI (gpt-image-1) or Google (Gemini 2.5 Flash Image) with an API key; a "Generate" button next to each character's avatar in the full editor builds the image from the character's Image Prompt and saves it as their profile picture. Local ComfyUI (no API key needed) is a planned follow-up. ### Fixed - **Fixed a real "NaNNaNNaN" rendering bug in the character card grid** — a stray double `+` operator (`+ + stat(...)`) coerced HTML strings to numbers instead of concatenating them. - **Character card avatars in the table view were rendering at full card-grid size (~90px) and overlapping the name text** — a CSS specificity bug where the base `.lib-char-avatar` rule (declared later in the file) silently beat the smaller `.lib-chars-tbl-avatar` override regardless of source order. Fixed with a properly-scoped selector; checkbox and avatar columns are also narrower now. - **The "Cast Characters" results grid and other browsing grids were rendering full detail cards instead of compact summary cards** — unreadable wall of expanded profiles. All cast-card grids (Library, post-generation results) now render the same compact card (name, occupation, archetype, main/side, lines, gender, voice, good/evil rating) and open the full profile on click. ### Changed - **The character table view is now sortable by clicking column headers** (Name, Gender, Age, Lines, Language, Alignment, Voice) — reuses the same comparators as the existing Sort dropdown, with a direction toggle and arrow indicator. ## [1.14.41] — 2026-07-10 ### Changed - **Character Sheets fields render as a compact list (label left, value right, thin separator) instead of stacked label-above-value blocks** — closer to the old flat field list while keeping the grouped 2-column sections. ## [1.14.40] — 2026-07-10 ### Changed - **Destructive confirmations (Identify all characters, New recast) now use an in-app dialog instead of the browser's native `confirm()` popup** — the native one exposed the raw server IP/URL ("192.168.178.8:7890 says…") and couldn't be styled to match the rest of the UI. - **A character with no generated fields yet shows a plain "profile hasn't been generated yet" note instead of a blank gap** — it was indistinguishable from data having disappeared. ## [1.14.39] — 2026-07-10 ### Changed - **The Character Sheets sidebar now defaults to sorting by most lines first** instead of by generation progress — main characters (who naturally have the most dialogue) surface at the top right away instead of waiting to be alphabetically or recently touched. ## [1.14.38] — 2026-07-10 ### Fixed - **"Waiting for streamed JSON output…" could get stuck forever even though generation kept completing passages normally in the background** — the fallback message only fired when the streaming request itself failed and fell back to blocking; if streaming succeeded but the model simply never emitted incremental deltas (some models buffer the whole answer into one final frame), nothing ever replaced the placeholder text, making a healthy run look permanently hung. ## [1.14.37] — 2026-07-10 ### Fixed - **The Passage/Live Output resize handle was inverted** — dragging up shrank the panel and dragging down grew it, backwards from what a top-edge handle should do (regression from moving the handle from the bottom-right corner to the top edge). - **"Lines" on the character header/sidebar was always blank during live generation** — line counts were only computed after the entire run finished; they're now tallied from the already-cast dialogue up front and attached to every sheet from the start, so they show immediately instead of staying blank until completion. ### Changed - **The Character Sheets sidebar is now a two-row card per character** — a bigger avatar spans both rows, the name on top, "N lines | filled/32" together underneath, instead of cramming avatar/name/count into one line. - **Character sheet generation could silently merge completely unrelated characters into one sheet** — the merge logic treated a shared title/occupation/age/race/language/social-class string as proof two characters were the same person, so different characters that happened to share a generic title (e.g. several villains here all carry "Verweser der von den Orks eroberten Reichsprovinzen") got smashed into a single corrupted sheet, with each wrongful merge dumping the other character's name into "Also known as". Matching is now based only on genuine name/alias fields. Sheets already corrupted by this need a regenerate (Cast Characters → New recast) to clear up — the fix only prevents it going forward. - **Character Sheets live preview now shows the exact source sentence behind each field** — click the small numbered mark next to a field to see the quote it was drawn from, instead of only a hover tooltip, making it easy to spot a misread at a glance. ### Changed - **Character Sheets live preview and card layout overhauled to match the richer casting Profil view** — 2-column grouped sections (Identität / Erscheinung / Persönlichkeit / Geschichte / Fähigkeiten / Konflikt / Beziehungen) instead of a flat field list, a full voice-picker row (Auswählen / Automatisch / Online suchen / Generieren), and the four prompt boxes (Voice Design, Image, SillyTavern, Concept Art) now only appear once actually generated instead of always showing "not generated yet" placeholders. - **Removed the duplicated Occupation/Archetype/Also-known-as lines in the character header** — they used to appear twice (once as plain text, once as pills further down); now shown once, as pills, in the position the plain text used to occupy. Added a "Lines" count and a highlight-color picker to the header too. - **The Character Sheets sidebar can now be sorted** — Progress (default), A–Z, Lines, or Gender, matching the sort control already on the casting roster sidebar. ### Fixed - **Character Sheets no longer pulls in stale characters from earlier casting attempts** — the sheet generation list was unioning the current live cast with *every* character record ever saved for the book, including junk entries ("Unbekannter Mann", "Turmwächter", etc.) from before recasts/merges/quality runs cleaned things up. It now only reuses saved records that still match a name in the current cast. - **Character line counts on the Character Sheets panel were silently always zero** — the code treated the roster (a plain array of names) as a lookup map, so counts never attached; now tallied directly from the segments. ### Changed - **The Reader's step wizard is now two rows instead of one horizontally-scrolling strip** — book title (with back-to-previous-step arrow) on top, the numbered step sequence below, so the steps aren't hidden behind a scrollbar. - **The Casting audiobook footer is now three grouped flyout buttons instead of eight flat ones** — "Identify Characters" (all / unknown / verify), "Cast Characters" (all / selected / new recast — a fresh rebuild that discards existing sheets), and "Cast" (view / export). "Open Script Rehearser" and "Continue casting" stay as direct one-click buttons. The flyouts are fixed-position and anchored to their trigger button (not nested in a clipping container), unlike the old dropdown this replaces a second time. - **The Character Sheets "Passage / Live output" panel now has a proper drag handle on its top edge** — a small centered grip bar you drag up/down to resize, replacing the boxed icon button that floated awkwardly over the bottom-right corner of the output text. ## [1.14.32] — 2026-07-10 ### Changed - **"Verify all characters" is now a real second opinion, not a rerun of the first pass** — it uses a distinct plausibility-check prompt that judges whether each existing label actually holds up (rather than reclassifying from scratch), now also re-examines every Narrator line specifically to catch spoken dialogue that got swallowed into narration, and resolves any name the model returns against saved aliases in code (e.g. "Vampire" → "Zerwas") instead of leaving alternate names as separate/Unknown speakers. ## [1.14.31] — 2026-07-10 ### Fixed - **Merging a character now only redraws the lines that actually changed speaker, instead of the entire feed** — a 2-line alias merge on a large book was rebuilding all ~2000 rows (each re-running the highlight regex pass), which is what made even a tiny merge take as long, and as freeze-prone, as opening a freshly cast book. Affected rows are patched in place now; only merges touching hundreds of lines still show the progress bar for more than an instant. ## [1.14.30] — 2026-07-10 ### Changed - **The characters sidebar now shows skeleton placeholder rows while a live cast is still reading and hasn't found any characters yet**, instead of a static "reading…" label, so it's obvious the panel is actively working rather than stuck. ## [1.14.29] — 2026-07-10 ### Fixed - **Restored the "Add alias / also known as" tag button on each character in the casting roster** — it was silently dropped from the roster row template during the recent Library cast card rework, even though the popup and merge logic behind it were still intact. ## [1.14.28] — 2026-07-10 ### Fixed - **Rebuilt the stale `static/dist/main.min.js` bundle** — `loader.js` always tries this prebuilt bundle before falling back to individual `static/js/*.js` files, so any edit to those files (however many restarts or cache-busts) had no visible effect until the bundle itself was regenerated with `npm run minify`. ## [1.14.27] — 2026-07-10 ### Fixed - **Reopening a large already-cast audiobook no longer freezes the tab** — restoring a saved cast with thousands of segments now redraws the feed in animation-frame batches instead of one giant synchronous pass, so the browser stays responsive instead of showing "Page Unresponsive" and looking like casting silently restarted on its own. ### Changed - **Character views now share a cleaner header and a wider overview card layout** — the Library cast cards are larger and more graphic, the profile header keeps the important identity info without duplicating aliases, and the audiobook profile view now shows line count with separate Auto refine and Edit actions. - **The audiobook footer buttons now read like the actions they perform** — Identify unknown characters, Identify all characters, Verify all characters, Cast all/selected character roles, View cast, export, and Script Rehearser are all direct buttons in a clearer left-to-right order, so the old flyout-style wording is gone. - **The Library cast table now uses a much smaller profile icon and tighter sticky columns** — the avatar no longer dominates the first columns, so the name and metadata scan more like a real table. --- ## [1.14.24] — 2026-07-09 ### Fixed - **Character sheet sources are visible again and appear before the prompt boxes** — the evidence section now renders any source metadata we have, instead of hiding everything that lacks a long quote. ## [1.14.23] — 2026-07-09 ### Changed - **The character-sheet header is less cluttered** — aliases now live in the identity block instead of being duplicated as header chips. ## [1.14.22] — 2026-07-09 ### Fixed - **The Character Sheets live layout no longer nests the side panel under the left column** — the output box and character roster are back in separate columns, and the output box keeps its visible resize handle when collapsed. ## [1.14.21] — 2026-07-09 ### Added - **A one-character refine action in the audiobook profile panel** so mixed profiles can be re-run on a single selected character instead of recasting the whole book. ### Changed - **Character Sheets and audiobook profiles now expose the same identity fields** — first name, last name, full name, aliases, gender, title, and occupation/profession are shown explicitly so one unified sheet layout is easier to read and edit. - **The live Passage / Live output box now has a visible drag handle even when collapsed** so the user can tell it is resizable and adjust how much of the streaming output stays visible. ## [1.14.20] — 2026-07-09 ### Changed - **Character Sheets now puts prompt generation at the bottom of each sheet** with collapsible ready-to-copy boxes for Voice Design, Character Image, SillyTavern Character, and Character Concept Art prompts. - **Character sheet evidence is easier to audit** — source hooks now show the supporting quote snippets, and alias labels are written as “also known as” in the UI. ### Fixed - **The cast-character flow no longer invents new profiles while refining an already cast roster** — selective recasts now focus on the existing cast and use nearby paragraph windows around each hit as context. ## [1.14.19] — 2026-07-09 ### Changed - **Cast Characters is now a dedicated page instead of a popup** — the audiobook cast flow now has a new `Cast Characters` stage between `Cast Audiobook` and `Cast`, with the old `Cast Characters` stage renamed to `Cast`. The new page keeps the cast-character content and actions, but uses the same large-page layout as `Cast Audiobook` so the UI is easier to work with. ### Fixed - **Conversation Playground thinking text was no longer visible while streaming** — the reasoning panel now auto-opens as soon as the first `thinking` chunk arrives, instead of staying collapsed until clicked. ## [1.14.18] — 2026-07-09 ### Fixed - **Every retried/recast audiobook cast created a new duplicate Rehearsal Library entry** instead of overwriting the previous attempt — a fresh (non-resume) cast unconditionally nulled the in-memory link to the existing SQLite record, even when retrying or recasting a book that already had one from a prior attempt. The link is now only cleared for a genuinely first-ever cast; retries and "Recast all" overwrite the existing record instead of piling up near-duplicates. --- ## [1.14.17] — 2026-07-09 ### Added - **Editable "Prompt" panel for Character sheets**, matching the one already on Casting audiobook — a button in the progress modal's header opens a textarea with the full extraction prompt (all ~25 fields + the sources/citation schema), with named-preset save/load/delete. Edits persist as the new default and take effect from the next passage onward (even mid-run), via a new `character_sheets_prompt` override accepted by `_charsheets_prepare()` server-side (shared by both the blocking and streaming endpoints, so it can't drift between them). --- ## [1.14.16] — 2026-07-09 ### Changed - **Redesigned the Character sheets progress modal** — was a small (420px) dialog with a collapsible single-line JSON preview and a cramped chip grid. Now a large panel (like the audiobook casting live view): the passage currently being analysed on the left, the LLM's live JSON output filling in on the right, and a character sidebar on the right edge with the character(s) just updated highlighted — so you can watch each sheet actually being built from its source text. --- ## [1.14.15] — 2026-07-09 ### Added - **Two new attribution rules for the casting prompt**: a mid-quote " - " (em-dash pause) no longer causes the model to treat it as the end of a quotation and hand the rest to a wrong/new speaker; and narration that names whose voice is about to speak (e.g. "Marcians Stimme wirkte...") now resolves the following unattributed line to that character instead of leaving it 'Unknown'. Applied to the default prompt and auto-migrated into already-saved custom prompts (same mechanism as the earlier Doppelpunkt-Regel upgrade). --- ## [1.14.14] — 2026-07-09 ### Fixed - **Clicking a name elsewhere in the text to feed an already-open "Assign to" popup did nothing** — a separate document-level "click outside closes the popup" handler only exempted clicks on the paragraph's own speaker label, not clicks anywhere else in the passage text. So the popup was being closed by mousedown before the (v1.14.13) name-click logic ever got to run. Clicks and drag-selections inside passage text no longer auto-close the popup. --- ## [1.14.13] — 2026-07-09 ### Fixed - **Clicking or drag-selecting a name could reassign the wrong paragraph** — the intended flow is: click the broken (Unknown/Narrator/misattributed) paragraph's speaker label first to open "Assign to", *then* supply the name by typing, clicking it anywhere in the text, or drag-selecting it. But clicking/dragging a name with no popup open yet used to open (and target) the popup for whichever paragraph that name happened to live in — silently reassigning that paragraph instead of the one the user meant to fix. Clicking/dragging a name now only ever fills an already-open popup; a paragraph is only ever selected via its own speaker label (or the existing double-click-a-name fast-assign shortcut). --- ## [1.14.12] — 2026-07-09 ### Added - **Character avatar images in the casting pickers** — the Characters Found sidebar, the "Assign to" popup's character list, and the "merge with an already-found character" list now show the character's saved portrait (same source as the profile detail panel) when one exists, falling back to the colour-keyed initial-letter dot otherwise. --- ## [1.14.11] — 2026-07-09 ### Fixed - **Clicking a name inside a paragraph's text silently retargeted the assign popup to that paragraph** — misleading when you open "Assign to" on one (e.g. Unknown) paragraph, then click a name mentioned in a *different* paragraph just to reuse it: the popup would jump to and highlight that other paragraph instead of staying on the one you meant to fix. Now, while a popup is already open, clicking a name elsewhere only fills the search box — it no longer changes which paragraph gets assigned. Clicking a paragraph's own speaker label still always retargets it, as does clicking a name inside a paragraph when no popup is open yet. --- ## [1.14.10] — 2026-07-09 ### Added - **Passage/Thinking panels in audiobook casting's live view are now resizable** — drag the bottom-right corner of either box to read more comfortably. ### Fixed - **Character-list collapse chevron pointed the wrong way** — it showed the direction opposite to where the panel edge actually moves. Now expanded shows → (collapsing moves the edge right) and collapsed shows ← (expanding moves it left). --- ## [1.14.9] — 2026-07-09 ### Fixed - **"Thinking" panels stayed empty for models served by some vLLM builds** (confirmed on `vllm-0.23.1rc1`) — those backends stream reasoning under a plain `reasoning` key instead of the more common `reasoning_content` key. Every place that reads reasoning text (Conversation Playground, audiobook casting's live view, character sheets) now checks both, via a shared `_reasoning_text()` helper. Root-caused by directly probing the model's raw streaming response and finding `"delta":{"reasoning":"..."}` instead of the expected field name. --- ## [1.14.8] — 2026-07-09 ### Changed - **Audiobook casting timeouts raised for large/slow models** — first attempt per passage: 6 min → 10 min; half-passage retry: 3 min → 5 min. 10 min also matches the server's own timeout cap, so this is the practical maximum without also raising that cap. --- ## [1.14.7] — 2026-07-08 ### Fixed - **Audiobook casting's "LLM Reading…" live view showed the passage three times** — once in the reader pane above it, once in a "Passage" column, and once more in a full-width block toggled by the chevron (which duplicated the "Passage" column exactly). Removed the redundant full-width block; the chevron now shows/hides the two-column view itself (previously it appeared automatically once the LLM's first token arrived and had no way to be hidden again). Swapped the column order to Passage (left) / Thinking (right) to match the Conversation Playground's layout. --- ## [1.14.6] — 2026-07-08 ### Added - **Collapsible "Thinking" panel in the Conversation Playground** — reasoning models' chain-of-thought is now separated from the actual reply: it streams into a collapsed panel behind a "Thinking" chevron (click to expand/collapse) instead of appearing inline in the chat or being spoken aloud. Handles both API styles: a dedicated `reasoning_content` delta field, and inline `<think>...</think>` blocks — including chat templates that inject the opening `<think>` as a fixed prompt prefix, so it never appears in the model's own stream (only the closing tag does). ### Changed - **Noise-gate slider moved next to the mic controls** in the Conversation Playground — it was sitting on its own row to the left of the text input; now it's grouped with Auto-stop/Hands-free/Live agent since it's a mic-input setting. --- ## [1.14.5] — 2026-07-08 ### Fixed - **"Add Custom" engine dialog was invisible/unclickable from the Speech Recognition and Language Models tabs** — two unclosed `<div class="card">` tags in the TTS settings markup left the dialog (and everything after it) nested inside the TTS panel's DOM subtree. Since that panel is `display:none` whenever another Engines tab is active, the dialog opened with zero size and no way to interact with it. Closed the missing tags so the dialog renders as a top-level element. - **Uncaught `ReferenceError: Cannot access '_localContainersLoadSeq' before initialization`** thrown on every page load when the Engines tab was active — a `let` declaration for the container-load sequence counter was placed after the code that first used it. Moved the declaration above its first use. --- ## [1.14.4] — 2026-07-08 ### Fixed - **"Apply effects" button stayed disabled after generating a preview** — streaming playback (the default, lower-latency mode) never produces an in-memory audio blob, and the button's enable logic required one. It now enables regardless of playback mode; if effects are applied before a buffered blob exists, one is synthesized on demand from the same voice/text/instruct/backend used for the preview. --- ## [1.14.3] — 2026-07-06 ### Added - **Rich voice-picker dropdown for App Routing's output voice field** — same searchable, avatar-thumbnail dropdown used elsewhere in the app, attached as a browse button next to the field. The field itself stays free-text so it can still target `vd_` Voice Design presets that aren't part of the voice library. - **Gender, Tag, Rating, and Group filters restored/added to the My Voices table** — the Gender and Rating filtering logic already existed in the code but its `<select>` elements had been left out of the visible layout after an earlier redesign (silently no-op'ing the whole filter-population function, which also broke the already-visible Language/Type dropdowns). Added the missing controls plus new Tag and Group dropdowns wired to the same filter state the sidebar's group/tag chips already use. ### Fixed - **Audio effects (reverb/compressor/chorus/pitch) failed with "pedalboard is not installed"** even though it was — `pedalboard`'s native extension links against `libatomic.so.1`, an OS-level shared library missing from the container image. Added `libatomic1` to the Dockerfile and rebuilt. ### Changed - **"edit ID" relabeled to make clear it's literally the filename** — the voice inspector's ID rename control already renamed the underlying `.wav`/`.meta.json`/`.reference.txt`/picture files on disk (via the existing `/api/voice/rename` endpoint); it just wasn't obvious "ID" meant "filename." Relabeled to "rename filename" / "copy filename" with clearer tooltips. --- ## [1.14.2] — 2026-07-06 ### Fixed - **Live streaming ("watch the LLM think") could get permanently stuck, silently breaking casting/character-sheets for the rest of the session** — confirmed in production: a stream held the shared attribution lock for 11+ minutes, causing every subsequent passage to be silently rejected as "busy" and fall back to the plain non-streaming view. This made it *look* like live-thinking had stopped working entirely, when actually one earlier passage never finished. Root cause: `requests`' `timeout=` on a `stream=True` call only guards the connection and first byte, not gaps between later body reads — if the LLM backend goes silent mid-stream, the blocked socket read can hang indefinitely, and since that's a native blocking call (not a Python-level `yield` point), neither a wall-clock check in the read loop nor the client disconnecting can interrupt it. Added a watchdog thread that force-closes the connection if a stream runs past its configured timeout, guaranteeing the shared lock always releases on schedule regardless of backend behavior. --- ## [1.14.1] — 2026-07-06 ### Fixed - **"Reading PDF… page N" indicator drifted toward the bottom of the screen on long documents** — it used `position: sticky` with a `top: 40vh` offset, whose containing block grew with every page appended during parsing, so the pill drifted downward instead of staying a fixed distance from the actual visible top of the viewport. Switched to `position: fixed`, anchored near the top regardless of document length. --- ## [1.14.0] — 2026-07-06 ### Added - **Watch Character Sheets fill out live** — the "Character sheets" progress dialog now has a "Live output" pane that streams the LLM's raw answer as it's written for the passage currently being processed, the same live-streaming approach already used for casting. Backed by a new `/api/character-sheets/stream` endpoint (sharing prompt-building and parsing with the existing blocking one), with automatic fallback to the blocking endpoint if the model/backend doesn't support streaming. --- ## [1.13.10] — 2026-07-06 ### Fixed - **"Cast as audiobook" icon was invisible** — a leftover rule colored its icon accent-blue from when the button was still the plain/secondary style; after making it the primary blue button, that same blue-on-blue rendered the icon invisible against the button's own background. --- ## [1.13.9] — 2026-07-06 ### Changed - **"Recast selected" no longer re-reads the whole book** — it now only sends the passages that actually mention the picked character(s) (matched against their name and known aliases), plus one chunk of surrounding context on either side for pronoun resolution, falling back to the full book only if nothing matched at all. Recasting one character out of dozens now takes a handful of passages instead of the whole thing. --- ## [1.13.8] — 2026-07-06 ### Changed - **Read Aloud toolbar decluttered** — removed the standalone "Character sheets" button (redundant with "Cast Characters" already in the Casting flow) and made "Cast as audiobook" the primary blue action, moved to the end of the toolbar as the clear next step. --- ## [1.13.7] — 2026-07-06 ### Fixed - **"Recast selected" showed every character as queued, not just the ones picked** — the progress dialog looked exactly like a full recast because it was passed the entire roster as the target list; the selection only actually took effect at the final save step. Now only the picked character(s) go in as the target roster, so the dialog accurately shows just those as queued. - **"Recast options" dropdown routinely opened off-screen** — its anchor button sits in the bottom action bar, so the menu opening downward (the default) was often pushed past the viewport edge, barely visible. It now opens upward when there isn't enough room below, and is styled more prominently. --- ## [1.13.6] — 2026-07-06 ### Fixed - **Large empty gap in Read Aloud's tuning panel** — the backend hint box sat beside the 4 tuning rows (Speed/Seed/Temperature/Native speed) and was forced to stretch to their combined height regardless of how short its own text was, leaving a big empty area below a 1-3 line hint. Moved it to full width below the rows, sized to its own content. --- ## [1.13.5] — 2026-07-06 ### Added - **Search + sort in the Casting sidebar** ("Characters found") — filter by name and sort by line count or alphabetically, matching the same controls added to the Library's character list. ### Fixed - **Merging characters could freeze the page long enough to trigger the browser's "Page Unresponsive" dialog** — the merge itself is a fast array loop, but redrawing the whole feed afterward (thousands of DOM rows, each running the highlight regex pass) was one long synchronous block. A spinner overlay alone couldn't fix this — it would freeze right along with everything else in the same JS turn. The redraw is now chunked across animation frames with a real, moving progress bar, so the browser stays responsive and the merge no longer looks like a crash on a large book. --- ## [1.13.4] — 2026-07-06 ### Added - **Merge two roster entries from the "also known as" popup** — when the LLM splits one character into two roster entries (e.g. "Darag" and "Schmied" for the same person), the alias popup now lists other already-recognized characters as pick-to-merge options, not just a free-text field. Picking one actually reassigns every one of its segments to the character you opened the popup from (not just a linked library alias that leaves the live cast still showing both), with full undo support. ### Fixed - **Character table view still misaligned after the earlier fix** — `display:flex` directly on a `<td>` (Stimme, Tags columns) broke its table-cell layout participation entirely in Chromium, rendering the cell stacked at the previous column's position regardless of table-layout mode. Moved the flex layout to inner wrapper `<div>`s and switched the table to `table-layout:fixed` with an explicit `<colgroup>` for good measure. --- ## [1.13.3] — 2026-07-06 ### Fixed - **Dragging a short quoted line ("»Henker«.") to split it opened the Assign-to-character popup instead** — any selection under 40 characters was treated as "assign this text as a character name," even when it was clearly a short quoted line (guillemets/quotes are never part of a name). Selections starting with a quote mark now fall through to the "Split text to Unknown Speaker" button instead of being hijacked into the assign popup. --- ## [1.13.2] — 2026-07-06 ### Fixed - **Character table view rendered as stacked blocks, not a table** — table rows reused the `.lib-char-card` class (for its click/voice/export wiring) whose `display:flex;flex-direction:column` turned every `<tr>` into a flex column, stacking its cells vertically instead of laying them out side by side. Reset to `display:table-row` and stripped the card-specific chrome (background, border, shadow) that leaked in with it. ### Changed - **One Generate button per prompt box** instead of a single "Generate" that always regenerated all four (Voice Design, Character Image, SillyTavern, Concept Art) at once. The server endpoint now accepts a `fields` filter and runs one independent LLM call per requested field — clicking one button no longer burns tokens on the other three, and each call's smaller JSON further reduces truncation risk. - **Sort dropdown for the character list** (Library → Characters/Cast) — Role/Alphabet/Number of lines/Gender/Voice assigned, persisted like the Cards/Table toggle. --- ## [1.13.1] — 2026-07-06 ### Added - **"View Characters" + recast options once a book's already cast** — Read Aloud's Casting view no longer offers a single blind "Cast Characters" button once this book already has saved characters. It becomes a split button: **View Characters** jumps straight to the Library's character overview, and the dropdown caret offers **Recast all** (previous behavior) or **Recast selected…**, which opens a checkbox picker of the existing cast and only refreshes the ones you check — everyone else's saved sheet is left untouched. (Extraction still re-scans the whole book either way, since any page could mention any character; "selected" just controls what gets saved afterward, not what gets read.) --- ## [1.13.0] — 2026-07-06 ### Changed - **Script Rehearser's Stage character list is now a sidebar**, matching Read Aloud's Casting sidebar look exactly (same `.ab-cv-side`/`.ab-char-item` styling: colored dot avatar, name, line count) instead of the old horizontal avatar strip above the transport bar. It sits alongside the script page, collapses to just the avatar dots via the same chevron control, and clicking a character scrolls the script to their first line. --- ## [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 `<think>...</think>` 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 `<think>...</think>` 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 <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 - **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 `<span>` 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 `<select>` options when the option set has not changed. - **Less hidden DOM work** — background voice-library refreshes update data without rendering the full voice list while the Voices section is hidden. - **Faster character render wiring** — replaces repeated `all.find(...)` lookups with an id map and batches production card insertion. ### Fixed - **Production bundle parity** — `scripts/minify.mjs` now includes `benchmark-voice-picker.js` and `library-characters.js`, matching the fallback loader order. --- ## [1.12.52] — 2026-06-30 ### Fixed - **Uncaught TypeError in settings.js** — `$('voice-id-input').addEventListener` crashed on load when the Clone section HTML hadn't been injected yet; guarded with `?.` --- ## [1.12.51] — 2026-06-30 ### Fixed - **Engines not loading** — `loadLocalContainers()` was called once at module load before the section HTML existed; now called every time the Engines section is shown and on every tab switch (all three tabs: TTS, STT, LLM) --- ## [1.12.50] — 2026-06-30 ### Added - **Skeleton loading for engine cards** — the LOCAL section in Engines (TTS / STT / LLM tabs) now shows animated shimmer placeholder cards while querying Docker container status instead of a blank box ### Performance - **Parallel Docker status checks** — `GET /api/local-containers` now queries all 25 container definitions concurrently via `asyncio.gather + asyncio.to_thread` instead of sequentially; reduces load time from ~25× latency to ~1× (one Docker socket call worth of time) --- ## [1.12.49] — 2026-06-30 ### Added - **SQLite persistence for characters and rehearsals** — character library and script rehearsals are now stored in `config/tts_creator.db` (SQLite) instead of browser IndexedDB; data survives browser-cache clears, works across browsers/devices, and is ready for future migration to PostgreSQL/Supabase - New REST API: `GET/POST/PUT/DELETE /api/characters/{id}` and `GET/POST/PUT/DELETE /api/rehearsals/{id}` backed by `core/database.py` - **One-time automatic migration** — on first load, if server DB is empty, existing IndexedDB data is automatically exported and imported to SQLite (characters and rehearsals separately) --- ## [1.12.48] — 2026-06-29 ### Added - **Server-side cast persistence** — after every casting chunk and after manual corrections, the full cast (segments + roster) is saved as `Scripts/cast.md` inside the book's server directory (`config/reader_library/<id>/Scripts/`); survives browser-cache clears and works across devices/browsers - **Cross-device restore** — `audiobookOpenCastView` now has a 3-tier restore: (1) in-memory, (2) localStorage, (3) server fetch; whichever fires first wins; server draft is cached into localStorage automatically - New REST endpoints: `GET/PUT/DELETE /api/reader/docs/{id}/scripts/{name}` and `GET /api/reader/docs/{id}/scripts` (list) ### Fixed - **Casting tab didn't restore draft on click** — the "Casting" sidebar nav item called `showReaderView('cast')` which only shows the panel DOM; now calls `audiobookOpenCastView()` which runs the full restore logic --- ## [1.12.47] — 2026-06-29 ### Added - **Skeleton loading UI** — the casting feed and "Characters Found" panel now show animated shimmer skeleton rows while waiting for results instead of a blank gray area - **Version shown instantly** — app version in About page is read from a `<meta>` tag immediately on load (no async delay); server fetch confirms it in the background ### Fixed - **Progress bar hidden before casting starts** — the 0% bar no longer shows in the initial "Ready to cast" state; it only appears once casting begins - **VERSION file updated** — the server-side version was stuck at 1.12.37; now correctly reports the current version after container restart --- ## [1.12.46] — 2026-06-29 ### Added - **Edit pencil in casting profile** — pencil icon in the top-right of the character header banner; clicking it enables inline editing of all character sheet fields (appearance, personality, history, skills, relationships) with 900ms auto-save; clicking the checkmark exits edit mode - **Sources in casting profile** — "Quellen im Text" section now appears at the bottom of the character profile; clicking a source page jumps to that page in the Read Aloud reader ### Fixed - **Progress bar hidden after casting** — the yellow progress bar and passage counter disappear automatically when the casting run is complete - **Profil button toggles** — clicking "Profil" when the profile is already open closes it (returns to script); the button label flips between "Profil" and "← Skript" --- ## [1.12.45] — 2026-06-29 ### Changed - **Casting character bar** — clicking a character in the casting view's character list no longer hides the feed; instead a compact bar appears above the feed showing the character's avatar, name, and dialogue-line count - All dialogue rows for the selected character are highlighted in the feed (subtle purple tint) - **Prev/next navigation** — up/down buttons in the bar scroll the feed to the previous or next line for that character, with a "3 / 76" position counter - **Search within character lines** — typing in the search box filters to matching lines and scrolls to the first hit; pressing Enter advances to the next match - **Open profile button** — the "Profil" button in the bar hides the feed and opens the full character sheet (voice assignment, alignment, history, etc.); the back button restores the feed and re-applies highlights - Re-clicking the same character in the list closes the bar --- ## [1.12.44] — 2026-06-29 ### Added - **Character detail in the casting view** — the "Characters Found" list in the Read Aloud casting panel now shows colored letter-circle avatars (matching the Library view colors) instead of small pill chips; clicking any character hides the cast feed and opens their full character detail (header, voice assignment, alignment, appearance, personality, history, skills, relationships) in the feed area; a "← Zurück zum Skript" button at the top restores the feed - **Voice assignment from casting panel** — Auswählen, Automatisch, and Online suchen voice buttons work inline in the character detail panel; after assigning, the panel refreshes with the new voice --- ## [1.12.43] — 2026-06-29 ### Fixed - **Character detail page scrolls properly** — `.lib-char-page` now has a fixed viewport height (`100dvh - 148px`) so the main content panel and the character sidebar each scroll independently; sidebar no longer floats outside or breaks sticky - **Sidebar clearly separated** — 3 px border + inset box-shadow replaces the thin 1 px line; sidebar is now 240 px wide with more padding - **Source quote → reader navigation always works** — clicking a source now always navigates to `s-reader` first; if the book is already open it scrolls to that page div (or finds the first sentence on that page via `readerJumpTo`); only shows a "reopen book" toast if neither source is available - **Auto-assign voice respects German gender terms** — gender detection now handles both English (`female`/`male`) and German (`weiblich`/`männlich`) by checking `startsWith('w')` as female in addition to `startsWith('f')`, preventing female voices from being assigned to male characters whose gender field was written in German --- ## [1.12.42] — 2026-06-29 ### Changed - **Character detail is now a full page, not a popup** — clicking a character card opens a two-panel detail page inside the Library section; center pane shows the full character sheet, right sidebar lists all characters in the same production sorted by appearances; "← Alle Charaktere" back button returns to the card grid - **All character fields are inline-editable** — every text field (name, aliases, archetype, physical, backstory, relationships, etc.) is `contenteditable` with auto-save (900 ms debounce after each keystroke) via `clPut`; the moral alignment bar is now a draggable `<input type="range">` slider - **Source quotes in character detail are clickable** — clicking a "Quellen im Text" source entry navigates to that page in Read Aloud (scrolls the reader to that page div); shows a toast if the book is not open yet --- ## [1.12.41] — 2026-06-29 ### Changed - **Portrait-style character cards** — cards are now vertical with a colored gradient banner at the top holding the 72 px avatar; name + archetype centered below; snippet, alignment bar, and relationship dots fill the body; compact voice row with "Auswahl" / "Auto" at the bottom; grid uses `minmax(200px, 1fr)` so more fit per row - **Voice assignment moved to top of character detail modal** — the "Stimme" row is now the first thing below the header, so voice picking is always visible without scrolling - **Quellen (sources) section added at bottom of detail modal** — shows verbatim text quotes with page number and context label from the character sheet generation; gives evidence for each character detail - **Stronger German language enforcement for character sheet generation** — system prompt now starts with a bilingual ⚠️ SPRACHE / LANGUAGE block listing every field that must be in German; the user-message reminder also now explicitly says no field may be in English ### Fixed - Existing character data generated in English can be refreshed by re-running "Cast Characters" from the production header — the language instruction is now strong enough that even stubborn models should comply --- ## [1.12.40] — 2026-06-28 ### Fixed - **Character detail modal had transparent background** — `var(--card)` is not defined; replaced with `var(--surface)` and `var(--panel)` throughout the modal ### Changed - **Larger fonts and spacing in character detail modal** — field values 15 px, field labels 11 px, section labels 11 px bold; alignment bar 12 px tall; avatar 90 px; alternating panel/surface backgrounds for section rows give clear visual separation - **Character detail modal fully translated to German** — all labels, section headings, tier names, arc descriptions, alignment descriptions, and button text are now in German (Erscheinung, Persönlichkeit, Geschichte, Fähigkeiten, Beziehungen, Konflikt & Strategie, Geheimnisse & Bogen, Moralische Gesinnung, Stimme zuweisen, etc.) --- ## [1.12.39] — 2026-06-28 ### Changed - **Character cards enlarged** — cards are wider (320 px min), font sizes match the rest of the app (15 px name, 13 px body), avatar is 52 px, interaction dots and alignment bar slightly bigger - **Click a card → full D&D/LARP character sheet modal** — shows all generated fields in labelled sections: Appearance (physical + clothing), Personality (mannerisms + voice pattern), Story (backstory, motivation, fears), Skills & Abilities, Relationships (full text + interaction dots), Conflict & Strategy, Secrets & Arc; moral alignment bar is larger with score and arc label; voice assignment buttons live in the modal too; Edit and Close buttons in the header; clicking the avatar inside the modal uploads a profile picture --- ## [1.12.38] — 2026-06-28 ### Fixed - **Characters/Cast showed empty (only "Import" button) after character sheet generation** — `_charRelsHtml` called `.toLowerCase()` on the `relationships` field which the LLM sometimes returns as an array instead of a string, crashing the card renderer mid-`forEach`. Only the toolbar bar was appended; all production groups were silently lost. Added `_libStr()` coercion in `library.js` for all rendered fields, and `_clSanitize()` in `characters-library.js` applied at both first-insert and merge time so stored data is always clean strings going forward. --- ## [1.12.37] — 2026-06-28 ### Fixed - **Progress dialog stuck showing "Cancel" after completion** — `.audiobook-overlay { display: flex }` in CSS overrides the `hidden` attribute, so `done()` calling `ov.hidden = true` had no effect. Fixed by using `ov.style.display = 'none'` / `'flex'` directly, same pattern as the cast panel fix. ### Changed - **Character sheets now navigates to Library → Characters/Cast on completion** — instead of opening a read-only overlay that requires closing, finishing character sheet generation saves all sheets to the library and immediately takes you to Library → Characters / Cast where you can see the cards with alignment bars, interaction dots, and profile pictures. If results are already cached, clicking "Character sheets" again goes straight there too. --- ## [1.12.36b] — 2026-06-28 ### Fixed - **Character sheet descriptions generated in English for German books** — the language instruction was a weak one-liner buried after a long English system prompt, so the LLM defaulted to English. Now the language rule is the very first thing in the system prompt, explicit about every descriptive field ("write all values in {language}"), and also repeated as a short reminder in the user message directly before the passage. Requires **docker restart** (Python change). Cache the new sheets by re-running "Character sheets" — cached results from earlier runs won't change. --- ## [1.12.36] — 2026-06-28 ### Added - **Profile picture on character sheets** — each character card in the overlay and in Library → Characters/Cast now has an avatar area. Click it to upload any image (PNG/JPG/WebP). The image is stored as a base64 data URL in the IndexedDB character record (`image` field) and survives re-generation. The letter-avatar falls back when no image is uploaded. Overlay cards show an 80×80 avatar beside the name; library cards replace the coloured initial dot with the photo. ### Fixed - **"Passage N failed: Error"** — the generic error message was caused by the JS not reading the HTTP status or response body before throwing. Now shows `HTTP 502: LLM character-sheet generation failed: <reason>` (or JSON parse error if the server returns non-JSON). Also logs the full error to the console for debugging. - **Context window overflow on early passages** — `csExistingSummary` was listing every missing field for all characters, which grew large quickly and could push the combined prompt over the model's context window. Trimmed to 5 key fields only (physical, backstory, motivation, voice_pattern, relationships). --- ## [1.12.35] — 2026-06-28 ### Fixed - **Character sheets used wrong mental model** — the generation treated character discovery as its primary job, starting with an empty roster and counting "found" characters as progress. But the 45 characters are already known from the casting run. Now `csForReader` seeds `csGenerate` with `_audiobook.roster` (all known names from the cast) from passage 1. The LLM sees the full cast on every chunk and focuses on filling in details rather than rediscovering. Progress shows **"X / 45 characters detailed"** (how many have at least one filled field) instead of the misleading "1 characters found". --- ## [1.12.34] — 2026-06-28 ### Added - **Alignment bar redesign** — replaced the text-based "◼ Evil → Good ◻" row with a proper gradient box (dark left = evil, light right = good). A white circle with a dark border marks the character's `moral_alignment_score` position. The arc direction arrow (↗ ↘ ↕ →) sits inside the circle and is colour-coded (green = redeeming, orange = descending, purple = complex). Arc note text appears below. - **Character sheet progress: character counter** — the progress overlay now shows a live "X characters found · Y complete" line below the passage progress bar, updating on every passage. - **Image prompt button** on each character card — copies a Stable Diffusion / ComfyUI portrait prompt built from physical description, clothing, archetype, gender, and moral alignment to the clipboard. - **Voice prompt button** on each character card — copies a voice design instruction built from voice_pattern, mannerisms, archetype, gender, and moral tone to the clipboard; ready to paste into Design a Voice. ### Fixed - **`[object Object]` in Physical / Relationships fields** — when the LLM returned an array or nested object for a scalar field the value was stored as-is and rendered via `String()` as `[object Object]`. Added `_csStr()` sanitiser that joins arrays with ", " and falls back to `JSON.stringify` for any other non-string value; applied at both initial insert and merge in `csMerge`. --- ## [1.12.33] — 2026-06-28 ### Fixed - **Blue box after "Recast Unknown → no unknown found"** — `audiobookCastView()` set `panel.style.display = 'flex'` as an inline style. When `showReaderView('main')` later set `cast.hidden = true`, the inline style won the specificity battle and the panel remained visible as a large empty box. Fix: clear the inline display style on hide, and stop setting redundant inline flex styles (the `.ab-castpanel-inline` CSS class already defines `display:flex; flex-direction:column`). --- ## [1.12.32] — 2026-06-28 ### Added - **Alignment bar on character cards** — each character card in Library → Characters / Cast now shows a compact good↔evil gradient bar with a white dot positioned at the character's `moral_alignment_score` (0 = evil / black end, 100 = good / light end). An arc arrow beside it shows the character's trajectory: ↗ (redeems), ↘ (descends toward evil), ↕ (complex arc), → (stable). - **Most-interacted characters** — below the alignment bar, up to 5 small coloured avatar dots show which other characters in the same production are most-mentioned in this character's relationships field. Each dot's colour matches that character's avatar and shows name + mention count on hover. --- ## [1.12.31] — 2026-06-28 ### Added - **Read Aloud submenu** — "Read Aloud" in the sidebar is now an expandable tree with two sub-items: **Reader** (PDF view) and **Casting** (jumps directly to the casting panel for the open book). ### Fixed - **Character sheets "passage 1/1" / No characters found** — `generation.js` (which defines `splitTextIntoChunks`) was missing from the production bundle. When the bundle was active, character sheets always fell back to sending the entire book as a single LLM call, which almost always returned nothing for large texts. `generation.js` is now included in the bundle (21 modules, 701 KB). - **Copy active → Copy selected** — the "Copy active" button on the voice library toolbar now copies the **checked/selected** voices when any rows are checked, and falls back to all active voices only when nothing is selected. Label and toast reflect which mode was used. --- ## [1.12.30] — 2026-06-28 ### Fixed - **Audiobook casting lost when reopening a book** — the cast autosave was stored under one global key and validated only by a fingerprint of the extracted text. Reopening a large PDF re-extracted text that drifted slightly, the fingerprint mismatched, and the whole cast was discarded as a "different document". Drafts are now keyed by the **book id**, so reopening the same book always restores its cast regardless of text drift (the text fingerprint remains a fallback for ad-hoc documents). - **Opening a book dropped into an empty casting panel** — a leftover cast panel from a previous session hijacked the reader via the nav restore logic. Opening a book now reliably lands on the PDF (main) view; the stale-cast restore is suppressed for a fresh open. Click "Cast as Audiobook" to restore that book's saved cast. ### Added - **Voice matching from the cast cards** — each character in Library → Characters / Cast now has, beside Pick/Auto, an **Online** button (opens Get a Voice Online on the Fish.audio tab, pre-filled with the character name + detected language) and a **Generate** button (opens Design a Voice pre-filled with the character's gender, language, name, and a voice description built from the sheet). - **Automatic language detection** — a dependency-free `detectLang()` (script ranges + stop-word frequency for de/en/fr/es/it/pt/nl) auto-fills the document language during character extraction when left on Auto, and drives the language pre-fill for online search / voice design. --- ## [1.12.28] — 2026-06-28 ### Added - **SillyTavern character-card compatibility** — import and export characters in the SillyTavern / TavernAI "Character Card" format. - **Import** accepts V1 (flat JSON), V2 (`chara_card_v2`), and V3 (`chara_card_v3`) cards as `.json`, plus `.png` cards with the JSON embedded in a PNG `tEXt` chunk (`chara`/`ccv3`). A gender heuristic fills the field when the card omits it. Available globally and per-production in Library → Characters / Cast. - **Export** writes a V2 JSON card per character (universally accepted by SillyTavern, Chub, AI Character Editor, …). The assigned TTS voice is stored under `data.extensions.tts_voice` so a round-trip preserves the casting. ### Fixed - **Huge empty box / content pushed to the middle** — when the voice API was slow and the 8-second loader fallback fired, it hid the loading skeleton with `opacity:0` but left the element in the DOM (still occupying ~700px) and caused later cleanup to skip removal. The skeleton is now fully removed from the DOM in every path, and `.sk-hidden` also sets `display:none`. --- ## [1.12.26] — 2026-06-28 ### Fixed - **Sidebar blue highlights on wrong items** — "Language Models", "Books", "Stage" were hardcoded `is-active` in HTML and stayed blue even when on a different section. Added CSS rule `[data-nav-section]:not(.active) + .nav-tree .nav-tree-item.is-active` to suppress sub-item highlight whenever the parent section-head is not current. ### Added - **Characters / Cast page** — Library → Characters / Cast is now a dedicated production-grouped view instead of redirecting to the generic Characters section. Shows all books/scripts that have characters, with each character displayed as a card containing their character sheet info (archetype, personality snippet), tier badge (Main/Supporting), gender icon, and a voice assignment row with Pick and Auto buttons. - **Voice picker in character cards** — "Pick" opens a compact inline popup with searchable voice list filtered by character gender. "Auto" assigns the highest-rated unassigned matching-gender voice automatically. --- ## [1.12.25] — 2026-06-28 ### Added - **Library skeleton loading** — Books and Theater Plays tabs now show animated placeholder cards while data loads instead of a blank panel. - **Cast Characters button** in audiobook completion footer — triggers character sheet generation from the casting panel. - **Characters/Cast nav** in Library now navigates directly to the Characters section. - **Return to cast audiobook** — navigating back to Read Aloud via the sidebar now restores the active casting panel if a cast session was in progress. --- ## [1.12.24] — 2026-06-28 ### Fixed - **Infinite skeleton hang** — if any script failed to load (`ERR_NETWORK_CHANGED`, DNS failure, etc.) the async loader threw, skeleton-removal code never ran, and the UI was stuck for the entire browser session. `init.js` and `nav.js` are now wrapped in try-catch; `_revealApp()` always runs. Added an 8-second hard timeout as an absolute fallback. ### Performance - **Bundle enabled by default** — loader now always tries `main.min.js` first (one request instead of 21 individual files). Falls back to per-file loading only if the bundle is missing. - **Eliminated `/api/version` blocking round trip** — version string is read from the already-loaded `style.css` link tag (synchronous, zero latency). Fetching `/api/version` was the first awaited call and blocked the entire load sequence. - **JS cache restored** — JS files now use only the version string for cache-busting (not `version-timestamp`). The session timestamp defeated the service worker cache on every reload; now the SW serves JS from cache on subsequent visits. --- ## [1.12.23] — 2026-06-28 ### Fixed - **Reader-main-view bleeds through casting panel** — `#s-reader #reader-main-view { display: flex }` overrode the UA `[hidden]→display:none` rule. When the audiobook cast panel is shown and JS sets `reader-main-view.hidden`, the element stayed rendered (as a 34px tall flex strip) and bled into the cast view. Added `[hidden] { display: none }` specificity override. - **Card collapse chevron invisible** — `.card-chev::before { content: '▾' }` didn't render in all browsers. Switched to `mdi mdi-chevron-down` MDI icon class, consistent with the rest of the UI. - **Sidebar sub-item highlights showing for non-current sections** — added CSS sibling rule so `is-active` sub-items (Stage, Books, Language Models) only show accent colour when their parent section IS the current page. - **Background process indicator** — `window.setNavBusy(sectionId, true|false)` now shows an animated dot on the nav item when Read Aloud is playing. --- ## [1.12.22] — 2026-06-28 ### Fixed - **`toggleNavTree is not defined`** — sidebar tree toggles can fire before `nav.js` executes on slow/interrupted loads. An inline stub now handles the click immediately (reads/writes `ttsvc_trees` in localStorage, toggles CSS classes directly); `nav.js` overwrites it with the full implementation once loaded. - **"Saved PDF data missing" dead end** — when a book's source PDF failed to upload at save time (e.g. network interruption), clicking the library card now opens a file picker so the user can re-select the original file. The PDF is re-uploaded to the existing book slot and the book opens immediately. --- ## [1.12.21] — 2026-06-28 ### Changed - **Casting feed — progressive expand** — clicking a "N lines hidden" divider now reveals only 20 lines at a time and leaves a new divider for the remainder, so the view stays anchored and the user doesn't lose their place. --- ## [1.12.20] — 2026-06-28 ### Performance - **Voice list sessionStorage cache** — voices are written to `sessionStorage` after every successful fetch (`ttsvc_vc`). On the next page load the list renders instantly from cache while the fresh fetch runs silently in the background. The Refresh button clears the cache first to guarantee a clean reload. --- ## [1.12.19] — 2026-06-28 ### Fixed - **Voice list flicker on load** — engines.js (batch E) calls `loadVoiceLibrary()` after nav.js already rendered voices, causing the list to blank out and reload. Second call now runs silently (no skeleton, no status reset) when voices are already present. --- ## [1.12.18] — 2026-06-28 ### Fixed - **Diagnostic code removed** — temporary debug IIFE and renderVoiceList try-catch scaffolding cleaned out of voice-library.js; My Voices loads correctly. - **Settings load crash: renderSettingsAbout is not defined** — `loadSettings()` called `renderSettingsAbout()` unconditionally but that function lives in `conversation.js` (batch E, loaded after init). Guard added with `typeof` check; the About section still renders when opened (nav.js already guards the same call). - **Duplicate element IDs** — `s-library.html` Characters panel duplicated `cl-book-filter`, `cl-search`, `cl-grid` from `s-characters.html`, causing `getElementById` to return the wrong element. Characters tab in Library now shows a redirect placeholder instead. - **Infinite recursion in loadVoiceLibrary** — `window.loadVoiceLibrary = () => loadVoiceLibrary()` overwrites the global binding that the arrow function references, causing immediate stack overflow. Changed to direct assignment `window.loadVoiceLibrary = loadVoiceLibrary`. --- ## [1.12.17] — 2026-06-28 ### Fixed - **Settings load crash** — `loadSettings()` used direct `.value` assignments on elements that no longer exist after the Library restructure; all now use null-safe `sv()` helper so a missing element is silently skipped instead of throwing. This was also preventing My Voices from loading on page start. --- ## [1.12.16] — 2026-06-28 ### Added - **Batch benchmark → My Voices sync** — after a batch benchmark run completes, results are automatically saved to each voice's `meta.json` via `POST /api/voice/meta`, and My Voices Factor + WPM columns update immediately without a manual page refresh. --- ## [1.12.15] — 2026-06-28 ### Added - **Lang + Gender in Benchmark batch results** — two new sortable columns after Voice. - **Lang + Gender in voice selection list** — flag emoji and gender symbol shown on each voice row. - **Sortable batch results table** — click any column header to sort; arrow indicator shows active sort direction. Defaults to Factor descending (fastest first). --- ## [1.12.14] — 2026-06-27 ### Changed - **Voices table**: simplified to only Factor + WPM from benchmark data (Length, Duration, Time removed — detail lives in Setup → Benchmark). - **Setup → Benchmark batch results**: now shows Duration, Factor (sorted descending, green ≥ 1.0×), Time, and WPM alongside existing Latency/Best columns. Factor replaces Avg RTF (same data, more intuitive direction). --- ## [1.12.13] — 2026-06-27 ### Changed - **Speed column split into Factor + Time**: the old `21.8s · 1.30x` cell is now two separate sortable columns — **Factor** (`1.30x`, audio÷render, colour-coded green/amber/red) and **Time** (`21.8s`, total render time). Column order: Length · Duration · Factor · Time · WPM · Seed · dBFS · Type · Source · Rating · Tags · Note · Active. --- ## [1.12.12] — 2026-06-27 ### Added - **Duration column** in the voice table — shows the length of the synthesised benchmark audio (e.g. `11.4s`), sortable. Distinct from Length (original clip) and WPM (speaking rate). - **Benchmark sentence presets** dropdown — four ready-made sentences (DE narrative, EN narrative, DE pangram, EN tongue-twister) plus a Reset option. Selecting a preset loads it instantly without overwriting anything else. ### Changed - **Column order** rearranged to: Img · Play · Name · Lang · Gender · Length · Duration · WPM · Speed · Seed · dBFS · Type · Source · Rating · Tags · Note · Active. --- ## [1.12.11] — 2026-06-27 ### Fixed - **Speed and WPM cells now update live** during a benchmark run — each row's cells are patched in-place immediately after its voice finishes, without waiting for the full batch to complete and the list to re-render. Same fix applied to the single-voice "Benchmark this voice" button. --- ## [1.12.10] — 2026-06-27 ### Added - **WPM column** in the voice table — shows how fast a voice speaks in words per minute, calculated from the benchmark audio duration and sentence word count. Sortable. Hover for a tooltip explaining the range (130–180 wpm is natural for audiobooks). - **Info (ⓘ) icons** on the Speed and WPM column headers with tooltips explaining what each metric measures. --- ## [1.12.9] — 2026-06-27 ### Changed - **Benchmark speed factor shows 2 decimal places** (`1.32×` instead of `1.3×`) in the SPEED column and live status line — more precise RTF comparison across voices. - **Sorting by Speed now sorts by the RTF factor** (faster voices first) instead of by raw synthesis time — voices with a higher `×` multiplier rank higher regardless of how long the benchmark sentence was. --- ## [1.12.8] — 2026-06-27 ### Changed - **SOURCE column auto-derives script name for Rehearser clones**: voices cloned from a Script Rehearsal now automatically display the script title (e.g. `Script`, `her`) in the SOURCE column, extracted from the note field (`Rehearser · <title> · <character> — …`). No manual entry needed; setting an explicit `origin` still takes priority. --- ## [1.12.7] — 2026-06-27 ### Added - **Editable Source field** in the voice inspector panel (below Note) — type a source label (e.g. `fish-audio`, `cloned`) and it saves immediately to the voice's meta.json. Changes now persist across refreshes. - **Bulk "Set source"** button in the multi-select toolbar — select any number of voices and apply a source label to all at once. - **Fish-audio tag fallback** in the SOURCE column: voices tagged `fish-audio` automatically show `fish-audio` as their source even without an explicit origin field, so the column is populated correctly without having to edit every voice. --- ## [1.12.6] — 2026-06-27 ### Added - **Audiobook auto-saves to Script Rehearser**: when casting completes, the result is automatically written to the Script Rehearser IndexedDB (same format, same library). The record is updated — not duplicated — whenever speaker corrections are made in the cast view. The record appears immediately in Script Rehearser → Library. - **"Edit in Rehearser" button**: replaces "Review & cast" in the completed cast panel. Opens the saved record directly in Script Rehearser with all speakers, voices, and page markers intact, ready to assign voices and synthesise. - Voice assignments made in Script Rehearser are **preserved** when the audiobook auto-saves again (only the script text and emotions are overwritten; voice/instruct/soul fields survive the update). --- ## [1.12.5] — 2026-06-27 ### Added - **Autosave for audiobook casting**: progress is saved to localStorage after every passage. A page refresh, browser crash, or accidental close no longer loses hours of casting work — reopening "Cast as audiobook" for the same document restores the session automatically with a banner showing how far it was completed and when it was last saved. Manual speaker corrections made in the cast view are also autosaved immediately. The draft is cleared when the script is explicitly saved to Script Rehearsals. --- ## [1.12.4] — 2026-06-27 ### Added - **Expandable context dividers in Recast Unknown**: the `⋯` gaps between scattered Unknown segments now show a count ("42 lines hidden — click to expand") and expand inline on click, revealing all segments between two Unknown passages so the user can see who is speaking before and after to make a better assignment. --- ## [1.12.3] — 2026-06-27 ### Added - **Casting activity indicator**: pulsing blue dot on the Read Aloud nav item while audiobook casting is in progress, so users can see it's working from any section. - **Restore cast view on return**: navigating away during casting and then back to Read Aloud automatically restores the casting panel instead of showing the blank PDF view. ### Fixed - **Character count out of sync on reassignment**: reassigning a segment's speaker now decrements the old speaker's count in the Characters Found panel, so totals stay accurate when corrections are made. Speakers that drop to 0 lines are removed from the panel automatically. --- ## [1.12.2] — 2026-06-27 ### Added - **PDF Search**: search input in the Read Aloud toolbar — press Enter to jump to the first page containing the term, Shift+Enter to go back. - **Noise gate slider**: adjustable minimum-amplitude threshold in the Conversation input bar (the blue marker on the level meter shows the current gate). Short noise spikes below the gate or bursts shorter than 300 ms are silenced before STT. Persists across sessions. - **Conversation stats panel** now has a collapse button (→) to hide the latency sidebar and give more chat space; a floating icon button restores it. ### Changed - **Sidebar tooltip** now extracts text correctly for all item types (sub-items that had no `.nav-label` span showed nothing before). - **Active section** in the sidebar now gets a blue background highlight + right-border accent (`.nav-tree-item.active` was previously unstyled, making it impossible to tell which section you were in). - **Casting audiobook**: LLM prompt now explicitly handles `?«` and `!«` as valid German quote endings, and instructs the model to treat an unclosed `»` at end-of-passage as dialogue. Deterministic fallback also handles the unclosed-quote edge case. --- ## [1.12.1] — 2026-06-27 ### Changed - **Sidebar icon-rail**: hover no longer flies out the whole sidebar (which caused the main content to jump left/right). Individual item labels now appear as a small floating tooltip next to the hovered icon, keeping the rail at a fixed 56 px and the layout completely stable. --- ## [1.12.0] — 2026-06-27 ### Added - **Slim icon-rail sidebar**: the button where the flag used to be collapses the sidebar to a narrow icon rail (desktop); hovering the rail flies the full menu out as an overlay (titles + nested items). State persists. - **Language picker moved to Settings → General** (was the sidebar flag). English / Deutsch. - **Collapsible settings panels** (using the app's standard `card` collapse style) to free vertical space: Conversation engine/prompt config, Read Aloud voice & synthesis settings (with the drag-&-drop inside), the Try It Out voice/playback box, and the Casting-audiobook LLM/prompt panel (prompt collapsed by default). ### Changed - **Read Aloud layout** reorganised: collapsible settings + drag-&-drop on top, zoom toolbar above the document, the document as the central area that **fits the viewport height**, transport + synthesis controls below it (always visible — only the document scrolls). - **Read Aloud "My Books"** card removed — saved books now live in the combined **Library → Books**. - **Conversation Playground** fills the viewport height (only the chat scrolls), and the config stacks full-width. - Collapsible-card initialisation now also runs when a section opens, so lazily-loaded sections get consistent collapse chevrons. ### Fixed - **Conversation microphone failed intermittently** ("EBML header parsing failed") in hands-free/live mode: a silence reset cleared the recording buffer in place, dropping the webm header so later utterances were undecodable. The recorder now restarts cleanly, keeping every utterance valid. (Recordings are also decoded in-browser to 16 kHz WAV, bypassing server ffmpeg.) Barge-in (interrupt the agent while it speaks) works via the **Live agent** toggle. - **Casting-audiobook overflow**: a long unbroken passage string widened the layout and pushed the sidebar off-screen; the feed now wraps and is width-constrained. --- ## [1.10.1] — 2026-06-27 ### Added - **Collapsible nested sidebar**: group headers *Voice Actions*, *Speak* and *Setup* are now expandable parent menus; **Tags** nests under *Library*, and *Integrations* (App Routing · Connect Apps) and *Settings* nest under *Setup*. Opening a section auto-expands its whole ancestor chain. ### Changed - **Conversation Playground** config stacks full-width — Speech to Text, Language Model, Text to Speech and System Prompt each on their own row for clarity. ### Fixed - **Conversation microphone failed with "Audio upload failed: Decoding failed / EBML header parsing failed"** on ARM64: recorded audio is now decoded in the browser and uploaded as 16 kHz mono WAV, bypassing the server-side ffmpeg webm parser entirely (falls back to the raw blob if browser decoding is unavailable). --- ## [1.10.0] — 2026-06-27 ### Added - **Combined Library** (new *Library* section under *Speak*) with three tabs — **Books**, **Theater Plays**, **Characters / Cast** — bringing audiobooks, rehearsals and the character roster into one place. Book and play cards cross-link: *Rehearse* a book, *Read Aloud* a play. - **Character tags** (like voice tags): each character carries a comma-separated list of productions, seeded with its origin book and editable in the character editor. A character can now be **reused across several books/scripts** — one record, many tags. - **Shared cast resolution**: opening a production in the Rehearser (and audiobook casting, which routes through it) auto-fills empty cast slots from the shared character roster, matched by book **or** tag. Cast voice choices are written back to existing characters on save, keeping the roster in sync. Productions are joined by normalized title (`prodKey`). - **Tags** navigation group under *Voices*: distinct voice tags with live counts, plus the *Cloned / Designed / Favourites* predicates, each filtering the voice list. ### Changed - **Sidebar restructured** into clearer groups: *Voices* (Library · Tags · Voice Actions), *Speak* (Quick Play · Conversation · Read Aloud · Script Rehearsal · Library), *Setup* (Benchmark · Engines · Integrations · Settings). "Try It Out" is now **Quick Play**; "My Voices" is now **Library**. - The standalone **Characters** section was folded into the combined Library; **Read Aloud** is now a single entry (its library lives in the combined Library); the Rehearser's Library/Cast steps moved into the combined Library, leaving Stage · Summary · Import/Export in the sidebar. --- ## [1.9.8] — 2026-06-27 ### Changed - **Seed Finder default sentence**: updated to a longer mixed DE/EN test phrase covering numbers with dots (3.567), compound nouns, umlauts, special characters, English technical vocabulary, time formats, and motivational prose — gives a more complete picture of a voice's character per seed. --- ## [1.9.7] — 2026-06-27 ### Fixed - **Voice inspector broken**: `curGender` was referenced before its `const` declaration inside `selectVoice()`, causing a temporal dead zone ReferenceError that silently prevented the inspector from opening. Clicking a voice now correctly opens the detail panel again. - **Batch operations ignore selection**: Calc dB, Precompute, and Batch Seeds now operate only on the checked (selected) voices when a selection is active, matching the existing behaviour of Benchmark. Previously all three always ran on all active / visible voices regardless of selection. --- ## [1.9.6] — 2026-06-26 ### Fixed - **Casting feed — scroll hijacking**: the feed no longer forces the view to the bottom while you are scrolled up reviewing or editing earlier lines. Auto-scroll only fires when you are already within 80px of the bottom. - **Casting feed — vanishing text**: the trimming limit was raised from 80 to 600 rows, and trimming is now suppressed while you're scrolled up, so older lines stay visible as long as you're looking at them. ### Added - **Casting feed — "↓ Live" jump button**: a floating pill button appears at the bottom of the feed whenever you've scrolled up. Click it to immediately return to the live bottom of the feed and re-enable auto-scroll. --- ## [1.9.5] — 2026-06-26 ### Added - **Casting view — page-break lines**: as the LLM processes a PDF audiobook, a **"Page N"** divider row now appears in the casting feed whenever the source PDF page changes. This gives a live view of where each page boundary falls within the script. - **Casting view — expandable LLM passage panel**: the "LLM Reading…" indicator now shows a preview of the passage being processed. A **chevron button** (▾/▴) expands it into a full scrollable view of the passage text so you can follow exactly what the LLM is reading and thinking about. ### Fixed - **Page numbers were off by one**: page-break markers emitted into the Rehearser script now use the correct 1-indexed PDF page number (was storing 0-indexed, so "Page 1" showed for what was actually the second PDF page). --- ## [1.9.4] — 2026-06-26 ### Changed - **Audiobook casting — page numbers in Rehearser**: page-break markers now carry the source PDF page number. In the Script Rehearser Stage, each page divider shows **"— Page N —"** instead of the generic "— Page break —", making it easy to cross-reference the audiobook script against the book. Applies both to audiobooks cast from Read Aloud PDFs and to screenplay PDFs imported directly into the Rehearser. --- ## [1.9.3] — 2026-06-26 ### Added - **Native Speed for Try it out & Read aloud**: a **Native Speed** control (range 0.5×–2.0×, default 1.0) is now available in both the *Try it out* and *Read aloud* sections. It passes the `speed` parameter directly to the TTS generation request (natively via the faster-qwen3-tts backend), producing audio at the target tempo from the model rather than using post-processing pitch/time-shift. Try it out persists the chosen speed in localStorage (per browser); Read aloud saves it with the document in the library (each book remembers its own speed). --- ## [1.9.2] — 2026-06-26 ### Fixed - **Audiobook casting — pagination preserved on save**: saving (or opening) a cast audiobook as a Script Rehearsal now keeps the book's **page breaks**. Read-aloud page boundaries are tracked through casting and re-emitted as `\f` markers at the nearest segment boundary, so the Rehearser paginates the saved script to match the source PDF instead of producing one continuous flow. --- ## [1.9.1] — 2026-06-26 ### Fixed - **Character sheets — "Connection refused" failures**: the analysis could send a dead `localhost:11434` LLM URL to the server (when app settings hadn't loaded yet), causing every passage to fail. It now never falls back to that hard-coded default — when no endpoint is explicitly chosen it lets the **server use its own configured `llm_url`**, so character-sheet extraction uses the same working LLM as the rest of the app. - **Audiobook casting — empty "Review & cast" preview**: opening the manual-correction preview after a cast rendered no lines because the name-highlighter (`highlightText`) was scoped to the live cast view only. It is now shared, so you can review, fix speakers/emotions, and open in the Rehearser again. ### Changed - **Script Rehearser / Stage — one text size**: narrator (action) text and spoken dialogue now use the **same font size** instead of mismatched 15px/16px, so the play reads evenly. - **Script Rehearser / Stage — text-size control**: a new **A− / A+** control in the Stage toolbar shrinks or enlarges the whole play (persists per browser), in both A4 and paginated/scroll views. - **Script Rehearser / Stage — collapsible character list**: the row of cast chips can now be **collapsed or expanded** via a "Characters (N)" toggle to free up vertical space. --- ## [1.9.0] — 2026-06-26 ### Added - **Character Library**: a new **Characters** section in the sidebar collects every character the LLM extracts into a persistent, browsable library (IndexedDB), grouped by the book or script they came from. Running **Character sheets** from Read Aloud or the Script Rehearser now auto-saves each character (keyed by *book + name*), merging in new detail on re-runs. Each card can be **edited** inline or **deleted**, and keeps its greyscale Good↔Evil alignment bar, arc arrow, page/line sources, and the 5-area psychological **Deep Analysis**. New module `static/js/characters-library.js`. - **Richer character sheets**: extraction now gathers six more book-derived fields per character — *Backstory & Origin, Relationships, Motivation, Fears, Mannerisms & Habits,* and a casting-focused *Voice & Speech* pattern (accent, pacing, register, verbal tics). - **Character sheets — morality at a glance**: each sheet now shows a **greyscale Good↔Evil alignment bar** (white = good, black = evil) with a 0–100 score, plus an **arc arrow** indicating whether the character stays put, *descends* (↘ good→bad), *redeems* (↗ bad→good), or follows a *complex* (↕) path. Also added **Clothing & Appearance** and **Capabilities** fields and richer physical detail (height, hair, eyes, skin, gait), with sources now carrying a short category **line hint**. - **Character sheets — Deep Analysis**: a per-character button runs a 5-area psychological & narrative study (Core Flaw & Desire · Agency & Passivity · Dialogue & Voice · Narrative Arc · Paradox & Depth) in a modal. New endpoint `POST /api/character-deep-analysis`. - **Audiobook casting — Recast & rescue tools**: after a cast run you can now **Recast all** (re-run the whole document with tweaked settings/prompt), **Recast unknown** (re-attribute only the leftover *Unknown* lines using surrounding context), run a **2nd Quality Run — Verify** pass that re-checks every speaker assignment and resolves Unknowns, and **Save script** straight to Script Rehearsals without leaving the page. ### Changed - **Recast unknown — passage separators**: re-analysing only the *Unknown* speakers now shows a `⋯` divider between non-adjacent passages, so it's clear where one excerpt ends and another begins. - **Dialogue attribution — fidelity & language**: the attribution prompt now forbids hallucinating/summarising (segments must reconstruct the passage word-for-word), emits emotion tags in the **same language** as the text (e.g. German *wütend/flüsternd*), and demands strict JSON. Removed the brittle text-script fallback parser that could mis-split prompt echoes into fake speakers. --- ## [1.8.1] — 2026-06-25 ### Added - **Audiobook casting — Warmup request**: Added an invisible "Wake up" request before starting the attribution loop, showing a clear "Waking up LLM model..." status. This absorbs the 3–4 minute cold-boot time of massive models (like 120B/30B via llama-swap) without freezing the UI or timing out the first real book chunk. - **Audiobook casting — Quick-cancel**: The character assignment popup now has a close button and responds to the `Escape` key, so you can easily dismiss it if you click "Assign" by accident. ### Changed - **Audiobook casting — Narrator quick-pick**: The `📖 Narrator` role is now always pinned to the top of the manual assignment dropdown list, so you don't have to scroll or type to revert a mis-cast line to narration. - **LLM timeout increased**: The hardcoded API timeout in `routes/conversation.py` for all LLM calls has been increased from 3 minutes to 10 minutes (`timeout=600`), providing plenty of headroom for dynamic model proxies to download and load models into VRAM on demand. ### Fixed - **Read Aloud — "Fit width" scaling bug**: Clicking "Fit width" on PDFs with a small cover page (e.g. A5) but larger subsequent pages previously zoomed the cover perfectly but blew the text pages up massively (e.g. 241%), forcing horizontal scrolling. The scale is now computed against the *maximum* width of all pages, guaranteeing the entire book fits. - **Read Aloud — Left-side text cut-off (CSS bug)**: Fixed a flexbox centering issue on the document container where a zoomed PDF page that was wider than the screen would overflow equally on both sides. Because browsers only scroll to the right, the left edge of the page was permanently inaccessible. Fixed by replacing `align-items: center` with `margin: 0 auto`. - **Audiobook casting — Minified bundle caching**: Features added directly to JS files weren't appearing because the app was serving an older, cached minified bundle. The bundle has been rebuilt and version cache-busting ensures the new UI shows up immediately. --- ## [1.8.0] — 2026-06-22 ### Added - **Seed Finder — pin a specific seed**: a direct *“fix this voice to a specific seed”* control (type a seed → **Pin**, or **Unpin** to go back to random) that saves straight to the TTS server without generating anything — handy when you already know the seed you want. - **Seed Finder — persistent pinned seed indicator**: the pinned seed is now saved locally in the voice's metadata and displayed prominently in the voice inspector header. It automatically restores the "Pin seed" input when reopening the voice. - **Seed Finder — batch all voices**: a **Batch seeds** button in the voice toolbar pre-generates and caches Seed Finder samples for every active voice. An **in-app dialog** (no browser pop-ups) lets you set the seed range with a live sample-count estimate, then shows live progress (current voice/seed) with cancel. Runs sequentially, **skips already-cached seeds** (resumable), and feeds the same cache the per-voice Seed Finder reads. ### Changed - **Seed Finder — samples are cached**: generated seed WAVs are saved in the browser (IndexedDB) keyed by voice + test sentence + backend, so reopening a voice shows previous results instantly and re-running only generates the **missing/failed** seeds instead of all of them. Added a **Clear saved** button to drop a voice's cache. - **Seed Finder — better test sentence**: a shorter default sentence that exercises all German umlauts (ä ö ü ß), numbers, and a few English words — quicker to generate and more revealing of a voice's character. ### Fixed - **Couldn't change a voice's displayed name**: the big name in the voice inspector was just the **last segment of the voice ID** (so `DE_F_Privat_Laura_01` showed as `01`), and double-clicking it renamed the *ID*, not the shown name. Double-clicking the header now edits a real **display name** (saved to the voice's metadata via `/api/voice/meta`, persists across reloads) and pre-fills with the current name; the separate **edit ID** button still renames the underlying voice ID. (Designed voices without a reference file don't support a stored display name yet.) - **About page showed `v0.0.0` and a stale changelog**: the deployment stack wasn't mounting `VERSION`/`CHANGELOG.md`, so the container had no live version file (fell back to `0.0.0`) and served the image's baked changelog. Both files are now bind-mounted in `docker-compose.yml` and `portainer-stack.yml`, so the About page reflects the running version and changelog. - **Seed Finder — “Failed to fetch” seeds**: long per-seed generations that intermittently dropped now **auto-retry** (2 attempts), each failed seed gets its own **Retry** button, and because successful seeds are cached, a second Run fills only the gaps rather than redoing everything. ## [1.7.0] — 2026-06-21 ### Added - **Seed Finder** — in **My Voices**, each voice's inspector has a 🎲 **Seed Finder** panel: generate a sample for a range of seeds (the TTS engine produces a slightly different take per seed), play each result, and click **★ Use seed N** to pin your favourite — saved immediately to the TTS server's `voices.json`. New `/api/tts-voice-seed` proxy, module `static/js/seed-finder.js`. - **Voice library — Precompute embeddings**: a **Precompute** button warms all active voices so the TTS engine builds and caches each voice's speaker embedding (`.pt`) ahead of time, making the *first* playback of a voice instant (better Time-To-First-Audio) instead of paying the one-time analysis cost on first use. Runs with bounded concurrency, progress, and cancel. (The faster-qwen3-tts engine already prefers a cached `.pt` and auto-creates it from the reference wav when missing — the wav stays the source of truth; this just pre-warms the cache.) - **Character sheets** — a **Character sheets** button in both **Read Aloud** and the **Script Rehearser** uses your configured LLM to extract actor-facing, RPG-style profiles for every character: Archetype, Physical Stats (metric-only), Alignment & Ethos, Core Attributes (highest/lowest), Trained Skills, Signature Inventory, Dark Secret / Fatal Flaw, Conflict Style, and Win Condition. Deduced details are marked with `*`, characters are grouped **main vs supporting**, and each sheet cites its **sources** (page number from the PDF + a short verbatim quote). Long texts are processed in chunks and merged per character (fields filled in, inventory/sources de-duplicated). Results render as scannable cards in an overlay with **Copy as Markdown**, and are cached so re-opening is instant. New endpoint `POST /api/character-sheets`, module `static/js/character-sheets.js`. - **Book → multi-speaker audiobook** — a **Cast as audiobook** button in Read Aloud turns a novel into a cast-able script. An LLM scans the current scope (selection / page range / whole book, chunked with a running character roster so the same speaker keeps one name throughout) and attributes every segment to a **Narrator** or a **character**, with a per-line **emotion**. New endpoint `POST /api/attribute-dialogue` and module `static/js/audiobook.js`; reuses the rehearser's casting, per-line tone, and synthesis. Failed chunks fall back to narration so a book always casts; progress is shown and cancellable. - **Live casting view**: while attributing, a **wide, non-blocking, minimisable panel** shows a scrolling feed of each line with its assigned speaker (and emotion) plus a **character roster that fills up** with per-character line counts — instead of a bare modal progress bar. You can **minimise it and keep using the app** (any tab), then re-open to watch progress; when it finishes it parks as a **"✓ Review & cast"** panel rather than auto-popping, so it waits for you if you wandered off. - **Calmer messaging**: a passage the LLM can't attribute is no longer shown as a red "attribution failed: Error" toast — it's a quiet "read by the narrator" note in the feed, with a single neutral summary ("N passages had no detected dialogue") in the review step. - **Editable preview**: before handing off, a review overlay lists every segment with an editable **speaker** (autocompletes from detected characters) and **emotion** so mis-attributions are fixed in seconds; "Open in Rehearser" applies the edits and lands you at the Cast phase. - **Audiobook export** (rehearser): an **Audiobook** button synthesises every cast line as MP3 (bounded concurrency, progress, cancel) and downloads **one MP3 per chapter** (split on Chapter/Kapitel/Part/Prologue… headings or act/scene markers), or a single file when no chapters are detected. - **Saved as a reopenable rehearsal**: handing the cast off to the Rehearser now also **saves it to the Script Rehearser library (Bibliothek)** automatically, so the attributed script + cast + per-line emotions persist — reopen it anytime to change speakers/voices/lines and synthesise or export the audiobook. - **Read Aloud** — a new sidebar tab that turns the app into a text-to-speech document reader. Import a **PDF** (rendered to its real page layout via pdf.js) or a **.txt / .md** file, pick any voice + backend and a **reading speed** (0.5×–2×), then press play: the document is read sentence-by-sentence while the **word being spoken is highlighted** in place (overlay box on the PDF page, inline highlight in text mode), with the view auto-scrolling to follow. Click any word to jump there. Reuses the rehearser's word-timing + pdf.js loader and the existing `/api/tts-preview` pipeline — no backend changes. New files `static/sections/s-reader.html` and `static/js/reader.js`. - **Read Aloud — synthesis-state overlay**: every sentence is colour-coded by state — **red** (not synthesised), **yellow** (synthesising), **green** (ready/cached), **blue** (currently reading) — shown as a translucent overlay on the PDF page and as a tint in text mode, with a legend. - **Read Aloud — PDF zoom controls**: Fit width, Fit height, Two-page spread, and zoom in/out with a live percentage. Word geometry is stored scale-independently so zoom re-renders instantly and highlights stay aligned; fit modes track window resizes. - **Read Aloud — resume**: the last reading position is remembered per document, so re-opening the same file resumes where you left off. - **Read Aloud — book library**: **Save to library** stores the original PDF/text together with its **synthesised audio** and reading position in the browser (IndexedDB). A "My books" shelf lists saved documents with audio- and read-progress bars; reopen one to continue right where you left off with the already-synthesised pages intact — handy for working through long books. Reading position auto-saves on pause / stop / leaving the tab. - **Read Aloud — MP3 storage & export**: audio is now synthesised and stored as **MP3** (far smaller than WAV, so books fit comfortably in the browser library). An **Export MP3** control downloads the synthesised audio as **one file per page** (sections combined) or **one file per sentence**, with meaningful filenames like `Title - p01 - 03.mp3` / `Title - p01.mp3`. Any not-yet-synthesised sentences in scope are rendered first. - **Read Aloud — voice consistency**: addresses the slight timbre/prosody drift you hear when each sentence is generated separately. A **"Voice consistency"** selector synthesises in larger continuous chunks — *per sentence* (responsive), *per paragraph* (steadier), or *per page* (steadiest) — so a whole passage is one generation. Optional **Seed** and **Temperature** inputs pin the generation (forwarded to backends that support them, with graceful fallback), **Normalise loudness** evens out volume between chunks on playback, and a backend hint flags cloned/zero-shot engines that re-sample per request and suggests remedies. Chunk mode + seed/temperature/normalise are saved with library books. - **Read Aloud — synthesise ahead**: a **Synthesise** button pre-renders audio for gap-free reading, scoped to **all**, a **page range** (PDF), or a **click-selected sentence range**. Select mode is a guided, persistent step flow — click a start sentence (it pulses as the anchor), then the end; the mode stays active with step hints so you can keep refining, and you leave it with the **Done** button or **Esc**. Synthesis runs with bounded concurrency, shows progress, and can be cancelled; the synthesis-state colours fill in green as each sentence completes. ### Changed - **Audiobook casting — smarter speaker attribution**: the LLM prompt now reasons about **conversational turn-taking** (in a two-person exchange speakers alternate, so untagged lines are attributed by context rather than dumped as “Unknown”), and each passage is given the **recent dialogue** from the previous one so a conversation continues correctly across passage boundaries. The deterministic fallback (used when the LLM is unavailable) also got a conservative two-person turn-taking fill and a stop-list that rejects common German non-name words (Sofort, Stimme, Frage, Plötzlich…), so it no longer invents bogus characters. - **Character sheets — self-filling across the book**: sheets now build up progressively — each passage receives the **sheet-so-far** (with which fields each character still needs) and the model fills gaps and refines instead of starting from scratch, so details accumulate as more of the book is read. - **Read Aloud — library now lives on the server (syncs across devices)**: saved books, their synthesised audio, and reading position were previously stored only in the browser (IndexedDB), so a book saved on the laptop never appeared on the desktop. The library now persists under the server's config volume (`reader_library/<id>/` with `meta.json`, the source document, and per-unit MP3s) via new `/api/reader/docs…` endpoints. Any device pointed at the same server sees the same "My books" shelf; opening a book is instant and its audio streams **per chunk on demand** (nothing is bulk-downloaded), and saves stay incremental (only new chunks upload). - **Language switcher** — replaced the sidebar language dropdown with a **flag toggle** next to the "Voice Creator" headline (click to switch interface language). Added German strings for **Read Aloud** ("Vorlesen") and its UI. ### Performance - **Read Aloud — memory & smoothness for long books**: decoded audio (uncompressed PCM) is now kept only for a small window around the playhead and re-decoded from the cached MP3 on demand; off-screen PDF page canvases are released and re-rastered on return — together these bound memory on big books (previously both grew unbounded and could crash long sessions). The next chunk is **pre-decoded** during playback for gapless transitions, transport actions no longer scan every unit (single tracked "reading" index), PDF sentences/units are built **incrementally per page** (no end-of-parse spike), and library **saves are incremental** — only newly-synthesised chunks are written (a separate per-unit audio store), instead of rewriting the whole book each save. ### Fixed - **Audiobook casting — German (and other) quote styles not recognised**: dialogue marked with German guillemets `»…«` / `„…“` / `›…‹`, French `«…»`, curly `“…”`, CJK `「…」`, or em-dash speech was treated as narration, so books like German novels cast everything to the narrator. The LLM prompt now explicitly handles all these styles (with guillemets called out), passages with **no quotation marks skip the LLM entirely** (so genuine narration isn't shown as a failure), PDF line-break **hyphenation is mended** (`Schwer- tes` → `Schwertes`) for clean speech, and if the LLM call fails on a passage that *does* contain quotes, a **deterministic fallback** splits out the dialogue **and attributes speakers from speech tags** (`»…«, sagte Riskan` → Riskan; pronouns rejected) so the book stays castable with real names even when the LLM is offline. - **Read Aloud — auto-scroll**: while reading, the view now scrolls only the document pane instead of the whole window, so the currently-spoken line no longer slides up under the app header out of view. - **Read Aloud — backend dropdown stuck on "Checking…"**: the reader's TTS-backend select is now populated by the shared backend refresh and fetched on demand when the section opens, so it fills reliably even if backends finish loading after you're already on the tab. - **Read Aloud — large PDFs froze the page** ("this page is not responding"): the page-parse loop now yields to the browser periodically (with a "Reading PDF… page x / n" indicator), and per-sentence status overlays are created lazily per page instead of all at once. A 60-page book now imports with a max main-thread stall of ~40 ms (was multi-second), creating only the visible pages' overlays. - **Chunked TTS — "Failed to fetch" on long text**: `splitTextIntoChunks` only split on sentence terminators (`.!?`), so newline-delimited text (e.g. German bullet lists or care-plan notes) was never split — the full page was sent as one request, causing a TCP timeout that the browser surfaced as "Failed to fetch". Fixed by processing each line individually before applying the sentence regex. Also moved `generation.js` from deferred batch E into the main feature batch so `generateChunkedTts` is always defined before the user can click Generate. --- ## [1.6.0] — 2026-06-03 ### Added - **Script Rehearser — Cast overhaul**: Card / List **view toggle**; **sort & filter** (name, gender, language, line count, tag); character-card-game styling (large portrait, name, description line, action row); **per-character online voice picker** (audition the match, browse alternatives, pick from your library, or search fish.audio inline); **"Hear a line"** button that synthesizes a representative one-liner from the character's own dialogue in their assigned voice. - **AI character notes** — Match local / Match online / Design all now research the play and drop a per-character note (description, gender, speaking style). - **Rehearser import auto-save** — uploading a script (PDF/text/FDX/Fountain) saves it to the Library immediately. - **Internationalization (i18n)** — interface **language picker** with **German** translation of the UI chrome; English is the source language (`static/js/i18n.js`, extend via `I18N_DICT`). - **Progressive Web App** — installable with offline app shell (`manifest.webmanifest` + network-first service worker), iOS web-app meta and safe-area support. - **Test suite** — Playwright smoke + functional tests (desktop **and** iPhone/WebKit profiles): app load, sections, clone tabs, PWA, rehearser parse→cast, bundle, i18n. - **Build tooling** — opt-in single minified bundle (`npm run minify` → `static/dist/main.min.js`, loaded when `?bundle=1`); architecture & migration notes in `docs/ARCHITECTURE.md`. ### Changed - **Accessibility → WCAG 2.1 AA** — accessible names on all controls, AA text/badge/button contrast, keyboard-focusable scroll regions (audited with axe-core; 40+ violations → a handful of edge cases). - **Performance / mobile stability** — GZip responses; `content-visibility` virtualization for long lists; lazy-loaded images; Rehearser caps decoded-PCM memory to a sliding window (fixes iPhone crashes); leaked `AudioContext` closed; bounded-concurrency bulk operations. - **Clone a Voice — reworked GUI** — integrated tab strip (Microphone · Upload · URL/YouTube), clearer sections, scroll-to + obvious "transcribing…" feedback, sample sentence keeps the typed name across language switches. - **Fish-Speech tone** — per-line tones now reach OpenAudio S2 via inline `[tag]` markers in the text (the `instruct` field is ignored by S2). - **fish.audio import** — de-duplicates voices already in the library and diversifies matches so different characters don't all get the same fallback voice. - **Get Voices Online** — tabbed, integrated source switcher; the scrape box lives only under "Direct sources". - **Voice library** — editable Voice ID (rename), complete country/accent list (decoupled from language), always-visible **Select all** toggle, redesigned bulk-delete confirmation modal. ### Fixed - **Screenplay parser** — title-page text, numbered scene headings (`A1 EXT. … EVENINGA1`), `OMITTED`/`CONTINUED` markers and dated page slugs are no longer detected as characters. - **Narrator & all voice pickers** now list the full voice library (lazy-loaded if needed). - **Cast list controls** wire reliably regardless of when the section mounts; role names no longer truncate; avatars enlarged. --- ## [1.5.0] — 2026-06-01 ### Added - **Fish-Speech TTS backend** — clones a voice's saved reference WAV (consistent identity) **and** honours inline emotion markers like `(angry)`, `(whispering)`, `(excited)` per line. The only backend that is both WAV-anchored and style-aware; the Rehearser prefers it when available. Configurable via `FISHSPEECH_URL`. - **Fish.audio Voice Library browser** (Get Voices Online) — search/filter the ~2M public voices at `api.fish.audio`, preview samples, and one-click **Import** (MP3 → WAV + reference transcript) → an instantly clonable voice. - **Cast tab redesigned as character cards** — big avatar, name, language, gender, tags, voice picker, voice-design prompt, "Character soul · LLM brief" with **Develop** (LLM), and per-character **Ignore / Hide / Delete**. - **Bulk-edit lines on the Stage** — a **Select** mode adds per-line checkboxes: **Ignore**, **Hide**, **Delete**, Un-ignore, Show-hidden. - **Designed voices** — display name is the **character name**, the script becomes a **tag**, and an auto-picked **gender/type avatar icon** replaces the language flag. - **Clone a Voice — name-first flow** — name first (drops into the read-aloud sentence), live voice-ID, **auto-transcribe** after trim, **auto-save when ready**, and a **File / URL / Microphone source picker**. ### Changed - **IMSDb scraper** — resolves the real script via each title's detail-page "Read Script" link instead of guessing a slug. - **Rehearser default backend** — `voice_clone` (then `fishspeech`) for consistent identity; the tone-warning explains the trade-off both ways. - **Try It Out** — the cramped voice/backend row is now a clean responsive layout. ### Fixed - **Narrator was silent** — `narratorVoice` now stays in sync with the narrator cast row. - **About → Changelog was empty** — `CHANGELOG.md` is now shipped in the image and resolved resiliently. --- ## [1.4.0] — 2026-06-01 ### Added - **IMSDb browser — list / cover view toggle** — switch between poster-grid and compact list view; preference persisted in `localStorage`. - **IMSDb browser — local catalogue cache** — catalogue is cached in `localStorage` for 6 h (matching server cache), making reopening the browser instant. - **IMSDb browser — title in fallback** — script title shown on each gradient poster card while the real poster loads. - **IMSDb browser — loading spinner** — animated spinner while the catalogue fetches. - **IMSDb browse button on Import / Export tab** — the "Browse IMSDb" button is now also available on the Import / Export panel; modal moved to global scope. - **Auto-design — detailed progress panel** — each character shows an expandable card during voice design: gender chip, language, voice ID, age, and the full LLM-generated character description with a live spinner. - **Auto-design — script title as voice tag** — designed voices receive the script title as their `tag` value so they're easy to filter/find. - **Auto-design — LLM endpoint datalist** — the LLM endpoint field is now backed by a `<datalist>` auto-populated from all configured Language Models engines, plus hardcoded defaults (Ollama, vLLM, LM Studio, llama-swap, LiteLLM). - **Stage — synthesis progress** — the synth bar is now more prominent (gradient fill, spinner, sticky), each synthesising line pulses with a blue glow, and the page auto-scrolls to the active line. - **Stage — tone warning banner** — when a non-style-aware backend (voice_clone, streaming, NVIDIA) is selected and tone is set on lines, a dismissable amber warning banner names the backend and suggests a style-aware alternative. - **Bulk-edit tools** — new sticky toolbar in My Voices: select any number of voices with checkboxes, then: **Set tag**, **Hide**, **Unhide**, **Rate**, or **Delete** in one action. - **Rehearser — Voice Design default** — the TTS backend picker in Cast now defaults to `voice_design` (style-aware) instead of voice_clone, so tone selections work out of the box. ### Changed - **Stage — edit button moved to right gutter** — the pencil (edit text) button is now stacked with the note button in the right-side gutter of each dialog block, keeping the block header clean. - **Tone / instruct order** — when an emotion is set on a line, the instruction now leads with a directive (`"Speak in a <emotion> manner. <voice profile>"`) so the model prioritises the tone over the base identity description. - **My Voices — hidden voices in sub-tabs** — fixed: Cloned, Designed, and Favorites tabs now respect the "Disabled" checkbox filter; hidden voices no longer appear unless explicitly requested. ### Fixed - **IMSDb covers showing as flat lines** — replaced `aspect-ratio` on a flex child (unreliable in all major browsers) with the `padding-bottom: 150%` wrapper trick, guaranteeing a correct 2:3 poster ratio. - **Rehearser TTS backend "No backend available"** — `refreshRehBackends` now triggers the global backend probe if `_ttsBackends` is empty, and registers a `_ttsRefreshHook` so the select stays in sync with the Engines page. --- ## [1.3.0] — 2026-05-31 ### Added - **Live mic monitor in Clone a Voice** — level-meter and scrolling oscilloscope waveform in the Microphone card. "Check level" / "Stop monitor" buttons, mic gain slider. Recording uses raw mic constraints (no echo-cancel / AGC). - **STT engine picker in Clone → Transcript** — pick any configured STT backend when auto-transcribing, bypassing an unavailable Whisper. - **Better sample texts** — all 8 languages rewritten to ~38 words / ~15 s, phonetically rich, proper Unicode diacritics. ### Fixed - **Empty "Read aloud" field** — sample text now reliably populates on load and when navigating to the Clone section. - **Recording quality** — `MediaRecorder` requests 256 kbps in Clone and STT→TTS. - **OGG file import** — explicit extension list in `accept=`. --- ## [1.2.0] — 2026-05-29 ### Added - **Remember last section on reload** — the active section (and Settings / Engines sub-page) is persisted in `localStorage`. A hard-reload (Ctrl+Shift+R) now returns to the same page instead of always jumping to My Voices. - **Conversation: live speech preview** — while recording, the active Whisper STT backend transcribes accumulated audio every 2.5 s and shows the result in the text input field in real time. The input is pre-populated with this live guess before the final Whisper result arrives. Also tries the browser's Web Speech API first (works on HTTPS / localhost) for even faster results. - **Conversation: Voice Activity Detection (VAD)** — recording now auto-stops after 1.5 s of silence detected via the Web Audio `AnalyserNode` RMS level. A "Sending in X.Xs" countdown appears in the status bar so the timing is visible. An **Auto-stop** toggle in the input bar lets users disable VAD and revert to click-to-stop. A thin audio-level bar below the status line shows microphone volume in real time during recording. - **Conversation: hands-free mode** — after the agent finishes speaking, the microphone restarts automatically. A **Hands-free** toggle (on by default) disables this; clicking the mic button manually always cancels any pending auto-restart. - **`scripts/release.py`** — automates version bump + CHANGELOG promotion. `python scripts/release.py --patch|--minor|--major [--dry-run]` renames `[Unreleased]` to the new version, updates compare links, writes `VERSION`, commits, and creates an annotated git tag in one command. - **Git pre-commit hook** (`scripts/hooks/pre-commit`) — warns (does not block) when `.py`/`.js`/`.css`/`.html` files are staged but `CHANGELOG.md` or `VERSION` are not. Run `bash scripts/install-hooks.sh` after cloning. - **`scripts/install-hooks.sh`** — one-liner to install the hook after a fresh clone: `bash scripts/install-hooks.sh`. ### Changed - **Config and logs are now bind-mounted local folders** — replaced the opaque named Docker volume with `./config/` and `./logs/` host directories. `portainer-stack.yml` updated with absolute host paths. - **Server writes a rotating log file** — `RotatingFileHandler` writes `INFO`-level and above to `./logs/app.log` (rotates at 5 MB, 3 backups). ### Performance - **Skeleton loading view** — `index.html` shows an animated shimmer placeholder immediately on first paint; fades out once JS finishes loading. - **Self-hosted WaveSurfer and MDI icon font** — removed render-blocking CDN requests; assets now served locally from `static/vendor/`. - **Parallel JS module loading** — restructured `loader.js` into 4 ordered batches; round-trips reduced from 17 to 6, 9 files fetched simultaneously. - **Version-based JS/CSS cache busting** — versioned assets served with `max-age=31536000, immutable`; bumping version invalidates the cache. ### Fixed - **LLM returned empty response (Qwen3 thinking mode)** — conversation turn now falls back to `reasoning_content` for think-only responses; error message hints to add `/no-think` to the system prompt. - **Engine settings lost after container recreate** — container names and URL overrides now persisted as server settings (`engine_container_names`, `engine_local_urls`); restored from server on first page load. - **Text-input turns returned 422** — changed `audio` form field to `Optional[UploadFile] = None` so text-only turns don't require audio. - **Conversation input bar hidden when mic unavailable** — warning box moved inside `conv-chat-window` so it never pushes the input bar off-screen. - **Browser caches old section HTML** — `loader.js` appends `?v=<timestamp>` to every section fetch. - Various import errors and container restart issues fixed. --- ## [1.1.0] — 2026-05-29 ### Security - **Fixed path traversal in `/api/browse-dirs`** — Added a `_BROWSE_BLOCKED` blocklist (`/proc`, `/sys`, `/dev`, `/run`, `/boot`). Requests for paths under these directories now return HTTP 403 instead of listing kernel/system files. - **Hardened yt-dlp output path** — After a YouTube download completes, the resolved output path is verified to be inside `TEMP_DIR` via `.relative_to()`. A file written outside the temp directory is rejected with an SSE error event and never registered. - **Removed CORS wildcard on `/api/proxy-audio`** — `Access-Control-Allow-Origin: *` was unnecessary (all callers are same-origin) and exposed proxied audio to arbitrary cross-origin requests. Header removed. - **Temp file registry now enforces a TTL** — `_registry` changed to `dict[str, tuple[Path, float]]`. `_registry_gc()` evicts entries older than `TEMP_FILE_TTL_SECONDS` (default 2 h, configurable via env var) and unlinks their files, preventing unbounded disk growth on long-running instances. ### Performance - **Settings and routing rules cached in memory** — `_load_settings()` and `_load_tts_routes()` previously read from disk on every API request (55+ calls per TTS synthesis). Both now use mtime-checked in-memory caches that invalidate automatically on write, eliminating redundant file I/O. ### Added - **Version number** — `VERSION` file at repo root; read by `core/constants.__version__` and surfaced via `GET /api/version`. Displayed as `v1.1.0` in Settings → About. - **Text input in Conversation Playground** — a pill-shaped text field and send button (→) sit left of the mic button. Pressing Enter or → sends text directly through the LLM → TTS pipeline, skipping STT entirely. Makes the playground fully usable without a microphone (HTTP context, no mic permission, remote access). The backend `/api/conversation/turn` now accepts an optional `text` form field; when set, the STT step is skipped and the STT latency row shows `—`. - **Container name field on all engine cards** — every TTS and STT engine card (Docker stack cards *and* static "Other Local" cards) now always shows the Docker container name input row. Previously absent/not-installed cards hid it; now it is always visible so the container can be pre-configured before starting. - **Connect / Disconnect toggle** — the Connect button now shows "Disconnect" (green, `check-network` icon) when already connected and toggles back on click. State persists in `localStorage`. - **Auto-apply on Connect** — a successful connection probe automatically saves the URL to Settings and makes the backend available in TTS/STT dropdown menus immediately, without requiring a separate "Use as TTS/STT" click. ### Changed - **Connect button redesigned** — moved out of the URL input row into a dedicated `dc-controls-row`. Restyled as a solid blue primary CTA (was a small teal outline button). Shows a spinner icon while probing. - **"Use as TTS / STT" button** — larger padding, bolder teal border, chevron icon, tooltip explaining it sets the URL in Settings. Gains `.active` highlight once applied. - **Unified controls row on every engine card** — consistent left-to-right order: `[Connect/Disconnect]` `[Stop | Start | Restart]` `[Use as →]`. Docker action buttons hidden until a container name is entered; Use-as button right-aligned. - **`initStaticDockerManagement`** — rebuilt to use the same `dc-controls-row` structure as the dynamic Docker stack cards. The existing `.llm-local-ping` button is moved from inside the URL row into the controls row at initialisation time. - **Backend refactor — `server.py` (5 560 lines → 43 lines)** — all logic extracted into single-responsibility modules: | Package | Module | Responsibility | |---|---|---| | `core/` | `constants.py` | Boot-time env defaults, path constants, version, log buffer | | | `registry.py` | TTL-based temp file registry | | | `validation.py` | URL validation, SSRF guard, path safety | | | `docker_client.py` | Raw Unix-socket Docker HTTP client | | | `config.py` | Settings load/save/normalize, backend URL resolution | | | `routing.py` | TTS route rules load/save/resolve, language detection | | | `audio.py` | Audio conversion, normalisation, auto-trim scoring | | | `voice.py` | Voice metadata, backup management, benchmark helpers | | | `presets.py` | Voice Design preset load/save, virtual voice resolution | | | `tts_helpers.py` | TTS request helpers, streaming, per-backend logic | | `routes/` | `admin.py` | Index, favicon, browse-dirs, robots, version | | | `settings.py` | `/api/settings`, routing rules, logs, design presets | | | `library.py` | All voice CRUD, upload, save, normalize, export/import | | | `stt.py` | `/api/transcribe*`, `/api/stt-backends` | | | `sources.py` | Voice scraping, proxy-audio, yt-dlp download | | | `docker.py` | `/api/local-containers/*`, `/api/probe-url` | | | `tts.py` | TTS preview, streaming, voice design, `/v1/*`, backends | | | `conversation.py` | Refine-text, effects, export/import, speak, MCP, conversation | `Dockerfile` updated with `COPY core/ core/` and `COPY routes/ routes/`. `docker-compose.yml` updated with `./core:/app/core:ro` and `./routes:/app/routes:ro`. - **Frontend refactor — `app.js` (8 744 lines → 16 modules)** — split into `static/js/` with `loader.js` loading them sequentially in dependency order: | Module | Lines | Responsibility | |---|---|---| | `utils.js` | 364 | Core helpers: `$`, `toast`, `escHtml`, theme, language/flag, picker, tabs | | `voice-inspector.js` | 397 | 3-pane voice workbench | | `voice-sources.js` | 277 | External voice source scraping UI | | `integrations.js` | 211 | Code snippet generation (SillyTavern, Open WebUI, HA, curl, MCP) | | `routing.js` | 542 | TTS routing rules editor | | `settings.js` | 385 | `loadSettings`, `applyAndSaveSettings`, settings panel | | `voice-clone.js` | 774 | WaveSurfer, drop zone, mic recording, trim, voice design | | `voice-library.js` | 2654 | Full voice library: list, CRUD, benchmark, normalize | | `tts-preview.js` | 528 | TTS preview, `fetchTtsPreviewBlob` | | `benchmark.js` | 218 | Performance + batch benchmark | | `stt.js` | 287 | STT→TTS playground, `refreshSttBackends` | | `init.js` | 49 | App bootstrap | | `engines.js` | 625 | ElevenLabs browser, custom engine cards, Docker management | | `ai-backends.js` | 520 | AI backend cards, LLM snippets, `initStaticDockerManagement` | | `generation.js` | 393 | WAV merge, chunked TTS, history, playlist, audio effects | | `conversation.js` | 520 | Conversation playground, LLM refinement, import, About | ### Fixed - **`chrome://flags/…` URL unreadable in mic-blocked warning** — the global `code { background: var(--panel) }` rule caused the URL text to render as white-on-light-grey inside the red warning box. Fixed with inline styles (`background: rgba(0,0,0,.35); color: #fff`) on the `<code>` element, plus a Copy button so users don't need to manually select invisible text. --- ## [1.0.0] — 2026-05-28 Initial feature-complete release. ### Added - **Voice library** — clone voices from audio samples; design voices from text descriptions using instruction-based synthesis; benchmark synthesis speed (RTF); normalize loudness; export/import voice packages as ZIP bundles. - **TTS backends** — Qwen3 TTS (Voice Clone, Voice Design, Custom Voice, Streaming), NVIDIA Magpie / Zeroshot / Flow, Kokoro FastAPI, VibeVoice, XTTS v2, ElevenLabs. - **STT backends** — OpenAI Whisper (port 8010), faster-whisper-server, whisper.cpp, Groq Whisper (cloud, free tier), NVIDIA Parakeet ASR. Real transcription probe in health check (not just TCP reachability). - **App Routing** — per-app / per-voice / per-language TTS routing rules with automatic language detection and optional before/after sound effects. - **Conversation Playground** — full STT → LLM → TTS pipeline with real-time SSE streaming, latency stats panel (STT / LLM TTFT / LLM total / TTS / Total), turn history, system prompt, and insecure-context warning. - **Engines section** — LLM / STT / TTS sub-pages; Docker container management (Start / Stop / Restart via Docker socket); custom engine cards; ElevenLabs voice library browser. - **Performance Benchmark** — single-voice and batch benchmark with RTF tracking, sparkline trend, and persistent history. - **Audio effects** — reverb, chorus, delay, compressor, gain, pitch shift (via `pedalboard`). - **Chunked TTS + generation history** — long-text synthesis split into chunks, per-chunk playback, playlist export as WAV. - **MCP server** — built-in JSON-RPC 2.0 endpoint at `/mcp`; tools: `speak`, `transcribe`, `list_captures`, `list_profiles`. - **LLM refinement & persona rewriting** — clean up STT transcripts or rewrite responses with a chosen persona via any OpenAI-compatible LLM endpoint. - **Connect Apps** — ready-made config snippets for SillyTavern, Open WebUI, Home Assistant, curl, and MCP (`claude mcp add` one-liner). - **Voice sources** — scrape voice assets from Aiartes, Freesound, GitHub, and Google Drive; YouTube download via yt-dlp; quick import directly to library. - **OpenAI-compatible proxy** — `/v1/audio/speech` and `/v1/audio/transcriptions` for drop-in use with Open WebUI, SillyTavern, and Home Assistant. - **Settings** — sub-pages: General, Connections, Playback, Captures, Payloads, Storage, API Keys, Logs, About. - **Voice Design presets** — saved persona templates for instruction-based synthesis; virtual `vd_…` voices usable from external apps without exporting WAV files. - **Multilingual support** — language/flag pickers, per-language preview texts, `LANG_FLAG_DEFAULT` mapping for 16 languages. - **Tags, ratings, and metadata** — per-voice tags with autocomplete, star ratings, gender label, country flag. - **Dark/light theme** — toggle with persistence in `localStorage`. - **Docker socket integration** — Start/Stop/Restart Docker containers from the UI via raw Unix socket HTTP; container health visible in engine cards. --- [Unreleased]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.58.0...HEAD [1.58.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.57.0...v1.58.0 [1.57.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.56.3...v1.57.0 [1.56.3]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.56.2...v1.56.3 [1.56.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.56.1...v1.56.2 [1.56.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.56.0...v1.56.1 [1.56.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.55.0...v1.56.0 [1.55.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.54.0...v1.55.0 [1.54.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.53.4...v1.54.0 [1.53.4]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.53.3...v1.53.4 [1.53.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.52.1...v1.53.0 [1.52.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.52.0...v1.52.1 [1.52.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.51.0...v1.52.0 [1.51.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.50.0...v1.51.0 [1.50.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.49.0...v1.50.0 [1.49.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.2...v1.49.0 [1.48.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.1...v1.48.2 [1.48.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.48.0...v1.48.1 [1.48.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.47.1...v1.48.0 [1.47.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.47.0...v1.47.1 [1.47.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.46.0...v1.47.0 [1.46.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.45.0...v1.46.0 [1.45.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.44.0...v1.45.0 [1.44.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.43.1...v1.44.0 [1.43.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.43.0...v1.43.1 [1.26.10]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.9...v1.26.10 [1.26.9]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.8...v1.26.9 [1.26.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.26.0...v1.26.1 [1.26.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.25.3...v1.26.0 [1.22.3]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.22.2...v1.22.3 [1.22.2]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.22.1...v1.22.2 [1.14.24]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.23...v1.14.24 [1.14.23]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.22...v1.14.23 [1.14.22]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.21...v1.14.22 [1.14.21]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.20...v1.14.21 [1.14.20]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.19...v1.14.20 [1.14.19]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.14.18...v1.14.19 [1.8.1]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.8.0...v1.8.1 [1.8.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.7.0...v1.8.0 [1.2.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.1.0...v1.2.0 [1.1.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/mARTin-B78/tts-voice-creator-clone-and-design-2/releases/tag/v1.0.0