# 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.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 `