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