Re-cast all three volumes end to end and compared each against its existing cast. Book 03 finished at 9 unknown, below the 25-line cast it replaced, and its fused multi-turn segments — one voice reading both sides of an exchange — fell from 60 to 6. Speaker counts stayed level with the hand-corrected books instead of inflating, confirming the phantom-speaker filter and the label consolidation both hold. Agreement with the hand-corrected books is 81-82%. The largest remaining error is systematic ping-pong inversion, where a whole exchange alternates onto the wrong speaker, followed by epithets that share no tokens with the canonical name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
350 KiB
Changelog
All notable changes to TTS Voice Creator — Clone and Design are documented here.
Follows Keep a Changelog · versioned with Semantic Versioning.
[1.20.8] — 2026-08-12
Added
- Fish Audio inline emotion-tag reference in Try It Out. Selecting a Fish backend reveals a panel with all 49 documented tags (24 basic + 25 advanced); click any tag to insert it at the cursor, plus an "Insert example" button that loads a multi-emotion sample. Shown only for Fish, since it is the only engine that reads bracket tags from the text itself — other engines would speak "[happy]" aloud as literal words.
Fixed
- Fish-Speech showed "Fetched 0 voices" and was unusable from the UI. It clones from the app's own saved WAV library, but was missing from the set of backends populated with those voices, so the code probed the Fish server for a voice-listing endpoint it does not have and got nothing. Now reports the full library (218 voices).
- Renamed the backend to "Fish Audio S2 Pro (Clone)" — verified the running checkpoint is
fishaudio/s2-proat revision1de9996b, matching upstream HuggingFace exactly. - Corrected Fish's advertised capabilities. It was listed as
style_aware: truewith "Strong: emotion/tone markers … are honoured per request" — measured to be wrong while cloning. With a reference clip, S2-Pro prepends it as an in-context example and imitates its prosody, which overwhelms inline tags: identical text showed a 10x loudness spread across emotions with no reference, but only 1.2x with one ([screaming] vs [whisper] was inaudible). Every lever was tested — canonical tags, free-form descriptions, repeated and mid-sentence tags, temperature to 1.0, truncated references,reference_idvs inline. The backend is now billed on what it does well: deterministic, byte-reproducible cloning.
[1.20.6] — 2026-08-11
Fixed
- Critical: the whole JS bundle stopped initializing partway through, silently disabling every module loaded after
tts-preview.js(Rehearser, Reader, Audiobook, character sheets, …). Introduced in 1.20.5 by the new emotion quick-pickers, which guarded withtypeof REH_EMOTIONS === 'undefined'— butREH_EMOTIONSis aconstdeclared later in the bundle's single shared scope, andtypeofon aconstin its temporal dead zone throws rather than returning"undefined". The pickers now readwindow.REH_EMOTIONSand initialize on a deferred macrotask, after the bundle has fully executed. This also invalidated earlier emotion A/B testing, which had been measuring a half-initialized app. - Emotion instructions are now always written in English, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest).
- Fish-Speech generation parameters (
temperature/top_p/repetition_penalty) were never forwarded. Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared_apply_tts_extra_paramshelper.
[1.20.35] — 2026-08-12
Fixed
- Phantom characters invented from German noun phrases. The caster emitted fragments of noun phrases as speakers — "in seinem Inneren" →
Inneren, "die Hand" →Hand, "mein Geliebter" →Geliebten, "der Wind" →Wind. Each became a cast entry with its own voice while the real speaker's line was lost to a phantom; measured against a hand-corrected book this was much of why a fresh cast produced 73 speakers where the human had 62. Such lines are now reset to Unknown — visibly unresolved and still recoverable by the repair passes — rather than silently wrong.
Validation
Whole-book re-casts of all three volumes, compared against their existing casts with scripts/compare_casts.py:
| Book | Fresh cast | After auto-repair | Existing cast | Phantoms |
|---|---|---|---|---|
| 01 | 286 unknown | 34 | 11 (hand-corrected) | none |
| 02 | 126 unknown | 7 | 0 (hand-corrected) | 5, then fixed |
| 03 | 265 unknown | 9 | 25 | none |
Book 03 finished below the cast it replaced, and its fused multi-turn segments — one voice reading both sides of an exchange — dropped from 60 to 6. Speaker counts stayed level with the manual casts rather than inflating, confirming the phantom filter and the label consolidation. Agreement with the hand-corrected books is 81–82%; the largest remaining error is systematic ping-pong inversion, where a whole exchange alternates onto the wrong speaker, plus epithets ("Amazone" → Lysandra) that share no tokens with the canonical name.
[1.20.34] — 2026-08-12
Fixed
- Repair passes could re-introduce the speaker label variants the cast had just consolidated. Consolidation ran only when casting finished, but every recast/repair pass assigns speakers of its own — confirmed live on a freshly cast book, which gained "Alrik" alongside "Alrik von Blautann" during auto-repair and would have split one character across two voices again. Consolidation now runs after each recast pass as well, and reports what it merged.
[1.20.33] — 2026-08-12
Added
- Speaker label variants are consolidated after casting. The caster refers to one character by several labels across a book — a bare name and a full one ("Sharraz" / "Sharraz Garthai"), a title alone ("Baronin" / "Baronin Ira von Seewiesen"), or a stray fragment ("Von" / "Oberst Alrik von Blautann"). Each variant became its own cast entry and therefore got its own voice, so a character audibly changed voice mid-scene — measured against a hand-corrected book, this accounted for more of the disagreements than genuine misattributions did. Variants are now folded together when one label's words are a strict subset of another's after stripping articles and titles, so "Weber" and "Alter Weber" stay separate and unrelated names never merge.
Fixed
- Quotation-mark toggle moved into the A− / A+ toolbar row, where it belongs.
[1.20.31] — 2026-08-12
Changed
- The quotation-mark toggle is now pure CSS. Guillemets are wrapped in their own
.ab-qelement at render time, so switching them off is a single class on<body>rather than rewriting every line's text and redrawing the whole feed — instant even with a couple of thousand segments, and the stored text is never touched. Also fixes the toggle appearing to do nothing on a freshly cast book: it previously only added missing marks and never hid the ones a book already had. The button moved to the bottom-right as a larger labelled pill, clear of the search controls it was overlapping.
[1.20.29] — 2026-08-12
Added
- Quotation marks in the casting script, with a toggle. German prose never quotes narration — only characters speak in
» «— so showing the marks makes a mislabeled line obvious at a glance. PDF extraction strips them from roughly 80% of segments, so they are now rendered rather than stored: dialogue rows are displayed wrapped in» «(lines that kept their own marks are left untouched), while the saved text stays exactly as cast so synthesis and exports are unaffected. The quote button in the casting view toggles them on or off, and the choice is remembered.
[1.20.27] — 2026-08-12
Fixed
- Two different characters' lines were being permanently fused into one segment. Adjacent segments are merged when they share a speaker, but an unattributed line carries the speaker "Unknown" — so two consecutive Unknown lines compared as "the same speaker" and were joined into a single block. Confirmed against the source text: Lysandra's line and Perdia's interruption, two separate paragraphs in the book, became one segment, after which no attribution pass could separate them and a single voice would have read both sides of the exchange. "Unknown" means the speaker is not known, never that two lines share one, so Unknown dialogue lines are no longer merged. Narration and lines with a real speaker name merge exactly as before.
[1.20.26] — 2026-08-12
Fixed
- Segments fusing two speakers' turns are no longer auto-attributed to one of them. A trailing inquit only names the last speaker, so applying it to a block containing several turns ("…ausliefern und …" + "Er hat es nicht so gemeint" + ", unterbrach die blonde Perdia Lysandra.") silently mis-credited the earlier turn — worse than leaving it Unknown, because a wrong attribution is invisible while an Unknown is not. Blocks holding multiple turns are now skipped by the grammar rules and left for the LLM pass or manual splitting.
[1.20.25] — 2026-08-12
Fixed
- A genitive name attached to "Stimme" after a quote now names the speaker ("Darrags Stimme klang tonlos." → Darrag). The existing voice-announcement rule only inspected the narration before a line, so this common mirror image was left Unknown.
[1.20.24] — 2026-08-12
Fixed
- Many German inquit verbs were missing from speaker attribution, so ordinary lines stayed Unknown:
grunzte,versetzte,unterbrach,drängte,herrschte,keuchte,hauchte,stöhnte,spottete,höhnte,erklärte,widersprach,warf ein,setzte hinzuand others. Also adds the thought verbsdachte/überlegte/sinnierte, since German inner monologue is punctuated like speech and belongs to the character thinking it. - Trailing inquits naming two people picked the wrong one. ", unterbrach die blonde Perdia Lysandra." assigned the line to whoever matched last; the grammatical subject is the speaker, so an optional article and adjectives are now skipped and the first name after the verb wins (→ Perdia, not Lysandra).
- A quote stayed Unknown when its own inquit was mistyped as dialogue. A segment opening with a lower-case speech verb ("grunzte der Ork und hob den Dolch.") is an inquit by construction, whatever type the caster gave it. Those are now recognised regardless of type, and the inquit segment is corrected back to narration.
[1.20.22] — 2026-08-12
Fixed
- A stale browser draft could silently destroy newer casting work. On opening a book the localStorage draft always won and was then pushed to the server as a "backfill", so a tab holding an older copy overwrote whatever was stored server-side — confirmed live: a cast improved from 79 Unknown lines to 25 was wiped simply by opening the book in a tab that still held the old draft. The two copies are now compared by their save timestamp, the newest wins (and is cached locally), and the server is only backfilled when the local copy is genuinely the newer one.
[1.20.21] — 2026-08-12
Fixed
- Split quotes with a pronoun inquit stayed Unknown. German dialogue routinely splits as
»Ja, ungewöhnlich«+", antwortete er knapp.", but every attribution rule required a literal name after the speech verb, so this whole class was never resolved. The antecedent is unambiguous, so it now resolves like the colon rule — to the last character named in the narration immediately before the quote. Verified against a real novel: resolves the reported case (→ Alrik) plus another, with no LLM call. - The "Repair mislabeled dialogue" pass used the wrong prompt unless launched from the menu. Called from Auto-repair or programmatically it fell back to the general casting prompt rather than the verification prompt, which is the one written for "is this narration actually speech?". It now defaults to the verification prompt in every entry path.
- Sentences split apart by quoted terms are rejoined. German uses
» «for titles and terms as well as speech ("…um »den gewundenen Weg der Krieger und Häuptlinge« handelte…"), and the casting model read those as a dialogue boundary, cutting the sentence into three fragments that no attribution pass could repair. Consecutive narration segments are now rejoined when the break is grammatically impossible, with a guard so genuine split quotes are not fused into narration.
[1.20.18] — 2026-08-12
Fixed
- Toolbar menus opened upwards and were clipped off the top of the screen in Studio. The Identify / Cast / Cast-view flyouts were hard-anchored to open above their button, which was right when those buttons lived in the casting panel's footer at the bottom of the page — but in Studio the same toolbar sits near the top, so the menu ran off-screen and its first entries were unreachable (confirmed live: "Auto-repair cast" and "Identify all characters" were both hidden). The menu now opens downwards whenever there is room, falls back to upwards when there isn't, is clamped inside the viewport, and scrolls if it is ever taller than the screen.
- Identify Characters menu order now matches the order the passes should be run in, with the one-click Auto-repair cast first.
[1.20.16] — 2026-08-12
Added
- "Repair mislabeled dialogue" — a new casting pass for spoken lines that PDF extraction typed as narration (usually because the
» «quote marks were lost). These were invisible to "Identify unknown characters", which only scanstype === 'dialogue', so re-running it could never fix them; the only pass that could ("Verify all characters") re-sent every narration segment to the LLM. The new pass scores narration for direct-speech signals (a bare inquit in the next segment, stray quote marks, imperative opening, 1st/2nd person, ending in!/?) and re-checks only the suspicious ones — measured on a real novel: 36 candidates instead of 1036, 29x fewer LLM calls. - "Auto-repair cast" — chains the repair sequence automatically (repair mislabeled → resolve unknown, repeated while it still helps, then a full verification only if still above target). Runs on its own after a completed cast when more than 10 Unknown speakers remain; never after a stopped run. Disable via
localStorage['ab-auto-repair-enabled'] = '0'. - German speaker-attribution rules 13–19 in both the casting and verification prompts: listener/vocative exclusion (a name beside a quote is often the listener, not the speaker), split quotes with pronoun inquits, ranked disambiguation among multiple candidates, continuation over ping-pong, unnamed actors as speakers, epithet resolution ("die Elfe" → Nyrilla), and splitting segments that fuse narration with dialogue or two speakers' turns.
Fixed
- A stopped casting run could silently leave the cast worse than before. The automatic rollback that undoes a pass which increases Unknown speakers was gated behind
!_audiobook.cancel, so stopping a run skipped it entirely — confirmed live: an interrupted pass took a book from 80 Unknown lines to 159 with no warning and no way back. Rollback now applies to interrupted runs too. - Corrected a misleading attribution rule. Rule 4 told the model that whoever acts before or after a quote is usually the speaker, but in German prose the neighbouring name is frequently the listener ("Marcian spürte ihre Tränen", "Marcian hörte darüber hinweg") or the person being addressed. It now applies only to active verbs, with perception/reaction verbs marking the listener.
- "Run until < N unknown" re-sent every narration segment to the LLM on every iteration (
includeNarrator), making each round enormously expensive and re-litigating lines that were already correct. It now uses the targeted repair pass. - Identify Characters menu reordered to the order the passes should actually be run in: identify all → repair mislabeled → identify unknown → run until threshold → verify all → check voice consistency.
[1.20.5] — 2026-08-11
Added
- Emotion quick-pick added to Read Aloud, Try a Voice, and Conversation. All three previously had either no style control at all (Conversation) or a free-text field that Fish-Speech silently ignores (Try a Voice, Read Aloud — it only reacts to an inline
[tag]in the text). The new picker is backend-aware: on Fish-Speech it's applied as an inline tag on the text itself; on style-aware backends (VoiceDesign, CustomVoice) it fills the existing instruct field as before. - "Run until < N unknown…" under Read Aloud's Identify Characters menu — repeats the recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a chosen target, or two passes in a row make no further progress (e.g. GPU/LLM contention). Previously only doable by calling
audiobookRecastUnknown()directly from the browser console.
Fixed
- Fish-Speech's per-line emotion tags were being read out loud as literal text instead of applied as silent tone control (confirmed live:
(excited)was spoken as "Hexited"). Fish-Speech's S2-Pro checkpoint requires square brackets ([excited]) for a tag to be treated as a control instruction — the app was sending round parentheses. Now that the user has Fish-Speech running (fish-speech-api, port 38080), the backend is confirmed working end-to-end: clones the reference WAV, keeps identity stable, and now actually applies the requested tone silently. Already auto-discovered under Engines → Text to Speech → Local, no configuration needed. - Character portrait prompts had no idea what kind of book they belonged to, producing things like a WWI/WWII-era naval admiral, East-Asian-styled warriors, and an Asian-presenting priest in a Western medieval fantasy book (confirmed live with screenshots from a German DSA fantasy novel). The book-level context feature (
/api/book-profile— genre/setting/era/language) already existed and was wired into voice-design prompts, but was never passed tocsBuildImagePrompt. Also now includes the character'srace_speciesfield (human/elf/ork/etc., already collected by the casting LLM but previously unused downstream) in the prompt. Applies automatically to future portrait generations once a book profile is set on Read Aloud → Book Profile. - Fish-Speech emotion tags produced no audible effect on non-English books (confirmed live on a German audiobook: audio quality was fine, but every line was flat/emotionless). Root cause was two-fold: (1) per-line auto emotions are LLM-generated in the book's own spoken language (e.g. German "bedrohlich"), but Fish-Speech's docs require English tags "regardless of the spoken language" — the untranslated German word was silently ignored; (2) a double-tagging bug: the client already embeds
[tag]directly into the text sent to Fish-Speech, but the server was also independently deriving and prepending its own tag from the separately-sent Qwen3-TTS-style instruct sentence, producing things like[Sprich in einem bedrohlich Tonfall.] [bedrohlich] …— neither bracket was valid English, so both were ignored. Fixed by (a) adding a German→English emotion-word translation table used when building the client-side Fish tag, and (b) making the server skip its own tag derivation whenever the text already carries one, while still falling back to the original short-text-verbatim behavior for freely-typed style instructions (Read Aloud / Try a Voice) that don't match either template. Qwen3-TTS backends are unaffected — they still receive the native-language instruct sentence unchanged, which is what they're designed to understand.
[1.20.4] — 2026-08-10
Fixed
- "Casting audiobook" and "Casting unknown" could finish all their LLM work and then get permanently stuck showing "Stop Casting", with no error and no way to proceed, even though the underlying work had genuinely completed (confirmed live: GPU load back to idle, but the panel never left its "running" state). Root cause: in both
audiobookCast()andaudiobookRecastUnknown(), the post-processing that runs after the main loop finishes (deduping, rollback checks, saving the draft) was completely unguarded — any exception there meant execution never reachedview.complete(), the only thing that actually resets the button and shows a result. Both now wrap that tail in their own try/catch, so a failure there still reaches a terminal state with a visible error instead of hanging forever. Note: this fix only applies to casting runs started after upgrading — a session already stuck in this state is running old code in memory and won't self-recover; reload and reopen the book, and the periodic autosave during casting means progress up to the point it finished should still be there to resume from.
[1.20.3] — 2026-08-10
Fixed
- Heading OCR now actually recognizes decorative chapter-heading images that combine an icon/border graphic with the text (e.g. a bold octagonal badge around "3.Kapitel") — previously it failed silently on every single one of these in a real test book (0 of 16 chapters recovered), either finding no text at all or confidently misreading the border as a stray character, because Tesseract's default full-page layout analysis gets confused by the graphic surrounding the actual text. Root-caused by testing the real OCR engine directly against the actual failing page: cropping tightly to exclude the graphic (roughly the bottom half of the heading region, where centered chapter-title text typically sits below any icon) and telling Tesseract to expect a single line of text (page segmentation mode 7) fixed it completely — verified against all 16 chapter headings in the same book, all recovered correctly and cleanly (1.Kapitel through 16.Kapitel, no garbage, no duplicates). Falls back to the original untrimmed full-heading-region OCR for headings that are already plain text with no surrounding graphic, so this is additive, not a narrowing of what already worked.
[1.20.1] — 2026-08-10
Fixed
- A PDF page that produces zero extractable text (no real text layer AND heading OCR either found nothing or failed the confidence threshold — common for a page that's entirely a decorative divider graphic, e.g. one book had a page that was just a small icon with no chapter number at all) used to be completely invisible to page tracking, silently shifting every subsequent page number out of sync with the actual PDF for the rest of the book. Confirmed live: investigated a real book's chapter-heading pages directly — one divider page rendered as literally just a small icon graphic (no OCR-recoverable text by design, not an OCR failure), and every such page was dropping out of
readerState.sentencesentirely, taking its page number with it. Every PDF page now keeps at least a placeholder marker (empty text — never spoken, never shown as a line) so page-break reconstruction after LLM speaker-attribution never skips a page number.
[1.20.0] — 2026-08-10
Added
- App Routing rows now have a playback-speed multiplier (0.5x-2x) and a preview button. The speed is applied server-side to real routed requests via a pitch-preserving ffmpeg tempo change (not a naive frame-rate shift, which would make a sped-up voice sound like a chipmunk) — see
core/audio.py:_change_tempo. The preview button synthesizes the row's exact output voice directly (no need to save the route first) and plays it back at the chosen speed for an instant "does this sound right" check, using a short phrase localized to the row's own language setting.
[1.19.4] — 2026-08-10
Changed
- Audiobook export now synthesizes lines one at a time instead of 2 concurrent workers. Every TTS backend this app talks to (Voice Clone, Voice Design, Fish-Speech, and the other local engines) is a single self-hosted GPU model instance, not a horizontally-scaled service — confirmed live, twice now, with two different backends: 2 concurrent requests reliably push at least one past the reverse proxy's 60-second timeout under real load, causing seemingly-random per-line failures that can doom a whole multi-hour export. Serial is slower per line but doesn't waste time on doomed, retried requests — net faster in practice, and actually finishes.
[1.19.3] — 2026-08-10
Fixed
- The looser chapter-heading detection from 1.19.0 caused ordinary narration sentences to be misdetected as chapter breaks, replacing real paragraph text with a thin marker line — confirmed live as "lots of empty pages" in the A4 pagination view. Two separate bugs: (1) requiring the keyword only at the start of the line, with nothing checked afterward, meant any short sentence starting with a common word like German "Teil" ("part") — e.g. "Teil des Grundes war unklar." ("Part of the reason was unclear.") — matched as a chapter; now whatever follows the keyword must actually look like part of a heading (empty, a bare number, or a colon/dash-separated subtitle), not a normal grammatical continuation. (2) The leading-numeral stripping regex treated a bare "C" as valid Roman numeral 100 with no requirement that anything sensible follow it — so it silently ate the "C" off the front of "Chapter", turning "Chapter 1: The Beginning" into "hapter 1: The Beginning" before the keyword check ever ran. Verified against the actual book that surfaced this (1998 lines): zero false positives, one correct real match ("14. Kapitel").
[1.19.2] — 2026-08-10
Fixed
- The tone/identity comparison table always suggested Voice Design as the alternative to Voice Clone, never Fish-Speech, even when Fish-Speech was running — the suggestion logic just took the first backend matching one criterion (
style_aware, oruses_wav) rather than preferring one matching BOTH, so it never surfaced the strictly-better option (tone-aware AND keeps voice identity) over a partial fix. Now prefers a backend satisfying both properties before falling back to a partial match.
[1.19.1] — 2026-08-10
Added
- Chapter headings now render as a visible horizontal-rule marker with the chapter's own title/number in the Script Rehearser/Studio Stage view, not just as an audible pause in the finished export. Reuses the exact same detection
audiobookExport()uses for chapter/file boundaries, so the fix to that detection (numbered OCR headings like "1.Kapitel") shows up here too — scanning through a long script now makes chapter breaks visually obvious instead of looking like one continuous, undifferentiated wall of narration.
[1.19.0] — 2026-08-10
Added
- Audiobook exports now have real pauses between paragraphs and chapters, plus an optional custom chapter-transition sound —
mergeWavBlobs()used to concatenate every line's clip with literally zero gap, which read as characters teleporting mid-scene with no beat between paragraphs, let alone chapters. New "Pacing" section in Studio → Perform & Export lets you set the paragraph pause (default 2s) and chapter pause (default 4s) independently, and upload a short sound (chime, page-turn, etc., max 2 MB) to play before each chapter's pause — decoded and resampled client-side to exactly match the narration's own sample rate so it splices in cleanly rather than corrupting the merge. Settings persist per-browser via localStorage.
Fixed
- Chapter headings recovered via OCR (from PDFs where the heading is baked into the page as an image) usually weren't recognised as chapters at all, because
audiobookIsChapter()required the keyword ("Kapitel"/"Chapter"/etc.) to be the very first word — but OCR'd numbered headings commonly read "1.Kapitel" or "I. Kapitel" with the number first. This silently defeated the documented "one file per chapter" audiobook export for any book using numbered image headings (confirmed live: a full novel exported as a single 7.5-hour file instead of per-chapter files). The chapter regex now tolerates an optional leading number or roman numeral before the keyword.
[1.18.23] — 2026-08-09
Changed
- Moved the backend tone/identity comparison table into the "Generate full audiobook" toggle row's spare width, instead of rendering as its own full-width banner below the toolbar.
[1.18.22] — 2026-08-09
Changed
- Redesigned the backend tone/identity warning as a proper comparison table (current backend vs. the suggested alternative, "Tone control" and "Voice stays identical" as columns with ✓/✗) instead of a run-on sentence with a "Switch" button awkwardly wedged into the middle of it.
[1.18.21] — 2026-08-09
Changed
- "Skip narrator" now defaults to unchecked (narration reads by default) and moved to the front of the Stage toolbar.
- The backend tone-support warning ("Switch to X for reliable tone…") now has an actual "Switch to X" button instead of just naming the better backend in a sentence and leaving you to go find it yourself in a settings dropdown.
[1.18.20] — 2026-08-09
Fixed
- A single transient synthesis failure could silently doom an entire multi-hour audiobook export.
fetchTtsPreviewBlob's own retry logic only covers connection-level failures —fetch()doesn't throw on a non-2xx HTTP response, so a backend hiccup (confirmed live: transient 500s clustered in the first ~50 lines, most likely GPU/engine warm-up contention from the two parallel export workers both starting cold) skipped that retry layer entirely and permanently failed the line. Different, completely ordinary lines failed across repeated attempts — never the same one twice — confirming this was never about any specific line's content. The export now retries a failed line up to 3 times with backoff before giving up on it for real.
[1.18.19] — 2026-08-09
Fixed
- Reverted the sticky "Generate full audiobook" toggle from 1.18.18 — it broke scrolling entirely. Stacking two sticky headers (the toggle plus the already-sticky, now-wrapping multi-row transport bar below it) could exceed the viewport height on a real window, leaving nothing scrollable visible at all. The toggle now scrolls away normally again, same as before 1.18.18; only the transport bar stays sticky. Will revisit with the wrapped toolbar's actual height accounted for, tested in isolation before shipping again.
[1.18.18] — 2026-07-30
Fixed
- The font-size (-A/+A) buttons were ~18px shorter than every other toolbar button (
.reh-fontsize-btnoverrode padding to 2px 7px against the base button's 9px 18px). Removing the override alone left a smaller residual gap (plain "A" text computes a shorter natural line-height than icon+text buttons) — matched the sibling buttons' actual rendered height directly instead of guessing at line-height multipliers. Verified live: exact match. - Studio's "Generate full audiobook" toggle wasn't sticky, so it scrolled out of reach while reading through a long script — only the transport bar below it was. Both now stick together, stacked in the correct order, so the mode toggle and play controls stay visible throughout.
[1.18.15] — 2026-07-30
Changed
- Renamed the Stage toolbar's "Skip desc." checkbox to "Skip narrator" — clearer about what it actually controls (whether the Narrator voice reads scene/action text during playback) now that the toggle genuinely works.
[1.18.14] — 2026-07-30
Fixed
- The Stage toolbar's font-size, page-mode, Train, and Exit buttons were unreachable on any normal (non-ultrawide) window.
.reh-console-actionshadflex-shrink: 0, and flex items default tomin-width: autoregardless of any width set — together these meant the block could never shrink below its full unwrapped width (~1560px), so the browser's wrapping algorithm never considered it "too wide to fit" and it just overflowed silently off the right edge with no scrollbar to reach it. Confirmed live at a realistic 800px window width. Now shrinks and wraps onto additional rows instead.
[1.18.13] — 2026-07-30
Fixed
- Narration playback ignored the "Skip descriptions" toggle (and Studio's "Rehearse ⇄ Audiobook" toggle, which just flips the same flag) once any narrator voice was assigned. The batch pre-skip optimization correctly checked both "skip mode is on" and "no narrator voice set," but the actual per-line narration-speak branch below it never re-checked the skip flag at all — so as soon as a book had a narrator voice configured (the normal case for any book actually being produced), narration played regardless of the toggle's position. This is why the toggle looked like dead weight: it couldn't turn narration OFF, only ever left it stuck ON. Both branches now consistently respect the toggle.
- Studio's audiobook-mode toggle now warns clearly if you turn it on with no narrator voice assigned yet, instead of silently doing nothing.
- Root-caused "emotions still not recognisable" on Book 02's audiobook: its entire line-audio cache (1761 cached clips) was synthesized before the per-line emotion-instruct engine fix was actually deployed to the running container — confirmed by comparing file timestamps against the fix's deploy time. Since the cache key is a hash of (text + voice + instruct) and none of those changed, every future playback kept serving the identical pre-fix, flat-delivery audio forever, with the engine fix having no way to ever take effect. Cleared the stale cache; the next playback or export for Book 02 will synthesize fresh against the corrected engine. Book 01's cache was unaffected — it was built entirely after the fix.
Fixed
- A voice that started as a Voice Design creation was routed to the Voice Design engine forever, even after it had a proper reference clip saved. Voice Design has no seed parameter at all, so every playback of a designed voice was an unpinned, unreproducible roll regardless of any seed pinned for it. Routing now checks only whether a reference clip exists (
has_ref) — the actual reason Voice Design is needed at all — not voice origin. A designed voice with a saved reference now clones like any other voice, which is what makes a pinned seed actually take effect for it. Applies to line playback throughout the app (Rehearsal, audiobook export, Try It Out) and to the Seed Finder's own backend default.
[1.18.11] — 2026-07-29
Fixed
- Designed voices now store their full design prompt. For a designed voice the prompt IS the voice's identity — the engine reproduces it from that text alone — but the only copy saved was the
notedisplay summary, which is deliberately clipped to 240 characters (43 of 73 voices were cut off mid-sentence). The complete prompt is now saved in its own field, so the TTS engine can register the voice from the whole description rather than a truncated one. Existing voices keep working from the clipped copy — it still carries gender, accent and timbre — and pick up the full text the next time they are redesigned.
Note
- This release pairs with an engine-side fix (in the
tts-dgx-spark-faster-qwen3-ttsrepo) for two bugs that made custom voices unusable: stale speaker embeddings causing cloned voices to ignore the requested text entirely, and designed voices never being registered with the Voice Design engine, which silently substituted a bundled British preset. See that repo's history for details.
[1.18.10] — 2026-07-29
Fixed
- A voice's pinned seed (set via the Seed Finder / Batch Seeds tools) was saved but never actually used — for two independent reasons. (1) The seed gets written to the voice's own metadata, but every normal generation call (Try It Out, Rehearsal, audiobook export) built its request without ever reading that value back — only the Seed Finder's own one-off benchmarking codepath looked at it. (2) Separately, the internal "voice_design_playback" stability profile (added in 1.18.4 specifically so ongoing dialogue wouldn't hardcode
seed: 0over every per-voice pin) wasn't recognized by the backend-name normalizer, which silently coerced it back to "voice_clone" — reintroducing exactly the hardcodedseed: 0it was designed to avoid. Together these meant a designed voice could still sound noticeably different (including gender-adjacent timbre drift) from one generation to the next even after "pinning" a seed for it — the pin was completely inert. Confirmed live post-fix: a Voice Design request for a voice with a saved seed now actually carries that seed. Every voice_clone/voice_design/customvoice/streaming request falls back to the voice's own saved seed when the request doesn't already specify one explicitly. - "Apply character persona" silently did nothing when the selected voice had no persona text saved (persona is a field you type manually on the Voice Inspector page — it's never auto-filled from a character sheet) — indistinguishable from the feature being broken. The checkbox is now disabled with an explanatory tooltip when the voice has no persona, and the backend returns a clear error instead of a silent no-op if it's checked anyway.
- Chunked preview generation (auto-enabled for text over 200 characters) dropped "Apply character persona" entirely, regardless of whether the checkbox was checked — it was hardcoded off for that code path only.
[1.18.9] — 2026-07-29
Fixed
- Highpass/lowpass filters (the new Telephone/Radio bandpass control from 1.18.7) were far too weak to be audible — confirmed via spectral analysis: pedalboard's filters are single-pole (~6 dB/octave), so the residual harmonic distortion left over from compression (see below) still poked straight through a single pass, leaving the "outside the passband" energy roughly unchanged or even higher than the original. Each highpass/lowpass now cascades 3 internal stages (~18 dB/octave) — confirmed live: content below/above the cutoff is now reduced by 7-14 dB instead of being essentially untouched.
[1.18.8] — 2026-07-29
Fixed
- The compressor's makeup gain (added in 1.18.5) was clipping. Confirmed live via spectral analysis: makeup gain routinely pushed peaks to ~1.9 (well past the ±1.0 digital ceiling), and the final hard clip in the effects pipeline sliced that overshoot off — producing broadband harmonic distortion that swamped the actual intended effect. This is why the Telephone/Radio presets still sounded like "no difference": the distortion, not the intended band-limiting, was dominating the output. Replaced the hard clip with a proper limiter after makeup gain, so peaks are caught smoothly instead of sliced.
[1.18.7] — 2026-07-29
Added
- Telephone/Radio effect presets now actually band-limit the audio — a new "Bandpass (telephone/radio)" control (high-pass + low-pass cutoff sliders) was added to the Try It Out effects panel and wired into both presets. Compression alone (even correctly calibrated) doesn't sound like "a phone call" — the defining trait is narrow frequency range, which the presets never applied and the UI never exposed.
[1.18.6] — 2026-07-29
Fixed
- The last fix for "Try It Out" showing only generic Voice Design presets instead of your own voices made things worse — it went from "wrong voices" to "no voices found." Voice Design's raw discovery endpoint only ever lists its own bundled presets, never any custom voice — so filtering that list against the user's own library (the same technique that correctly works for Voice Clone/Streaming) always produced an empty intersection. Fixed properly: for Voice Design, the dropdown is populated directly from the user's own active voice library instead of trying to filter the engine's useless discovery list at all.
[1.18.5] — 2026-07-29
Fixed
- Browser Back/Forward didn't work inside the app at all — every section change called
history.replaceState, which overwrites the SAME single history entry instead of adding a new one, so the browser had nothing from the app's own navigation to step through. Clicking Back skipped straight past the whole app to whatever page was open before it. Switched topushState; the existing hashchange listener already handled Back/Forward correctly, it just never had real history entries to respond to. - "Try It Out"'s Voice Design dropdown only ever showed the engine's own built-in presets (vd_british_male, vd_german_male, ...) — never any of your own designed voices, even after designing 70+ characters for a book. Now filtered to your own voice library, same as Voice Clone/Streaming already were.
- Audio effects had no audible impact — root-caused live: every effects preset's compressor threshold sat ABOVE where our own TTS output actually lives (all voices are normalized to ~-20dBFS; presets ranged from -10dB to -18dB), so a compressor threshold-gated to louder signal had almost nothing to act on — confirmed the "Telephone" preset changed a real speech clip's loudness by under 0.1%. Lowered every preset's threshold to actually engage with real output, and added makeup gain after compression (a plain compressor with no makeup gain only ever shaves peaks quieter — it never produces the louder, "punchier" sound people associate with compression, which is why even a correctly-engaging compressor was hard to notice).
[1.18.4] — 2026-07-29
Fixed
- Several characters were split across multiple speaker labels with inconsistent — sometimes wrongly-gendered — voices: the underlying book text refers to the same person by different names/titles/epithets at different points (a vampire lord called "Roger," "Zerwas," and others; a character nicknamed "Irre" who is really "Uriens"; a guard referred to once as "Wächter" and once as "Wächter des Turmes"), and each distinct label had gotten its own independent cast entry and voice. Confirmed live: merged 8 such groups onto one consistent voice each. Four of them ("Inquisitor," "Jägerin," "Linosch," "Sharraz" — 61 lines total) had NO voice assigned at all, meaning that dialogue was being silently dropped from the audiobook entirely, not just mis-voiced.
- Emotion stopped coming through audibly right after the accent-reinforcement fix landed — root cause: Qwen3-TTS's own prompting guidance warns it doesn't reliably follow multiple conflicting instructions in one prompt, favoring one over another. The accent clause was being placed FIRST in every line's instruct, making it the most prominent instruction on every single line and likely crowding out the (shorter) emotion tag that followed it. Reordered so emotion leads and the accent reminder trails, instead of the other way around.
[1.18.3] — 2026-07-28
Fixed
- The anti-American-accent instruction only ever applied to the one-time call that designs a new voice — never to any of the actual lines it goes on to read. Root cause: the accent clause was built fresh at design time and layered on top of the character's saved voice-quality description, but never written back into that saved description — so every ONGOING line's instruct (built from the saved profile) carried zero accent guidance. Since each synthesis call is stateless, the model has no memory of the original design call's instructions; omitting the clause here meant real narration got none of it at all, only the initial creation did. The same accent clause now gets added to every line's instruct, not just the design call — Stage playback, Synth all, and the audiobook export all benefit automatically since they share the one
_buildInstructfunction.
[1.18.2] — 2026-07-28
Fixed
- Seed Finder's default test sentence for German (and every other) voices was itself a mid-sentence mix of German and English — confirmed as the actual cause of "the seeds are horrible, that's a mixture of English and German": the
DE,EN, and "mixed torture-test" constants were all literally the same string, including a full English sentence ("The system administrator successfully configured the customized Docker stacks...") baked into the "German" default. Replaced with genuinely single-language defaults per voice, and — better — the voice's own saved reference transcript (real book content) is now used first when available, since that's exactly what the voice will actually read. - Designed-voice playback never told the engine what language to expect, unlike every other backend — left at "Auto" (auto-detect) for every single line, which is least reliable on short dialogue lines. Now passes the voice's own language explicitly, same as Voice Clone/Streaming/CustomVoice already do.
Investigated
- Tested whether an already-designed voice could be served through the Voice Clone engine instead (the officially-documented path for consistent multi-line reuse) — the clone engine doesn't recognize a Voice-Design-only voice_id at all (confirmed live, before and after a full backend restart), so this would require a proper conversion step this app doesn't yet implement. Not pursued further this round; noted for a future pass.
[1.18.1] — 2026-07-27
Fixed
- The previous loudness fix (normalizing every clip independently to one target level) had a real side effect: it erased a voice's own whisper-vs-shout dynamics along with fixing the cross-voice level gap. Confirmed live: a whispered line came out LOUDER than its own neutral reading once both were pushed to the same target — exactly backwards. Replaced with a fixed per-voice gain offset (the voice's own already-computed reference gain from Calc dB, applied uniformly to every line from that voice) — this shifts each voice's baseline to match others without touching how loud one line is relative to another from the same voice.
- Audited all 72 designed voices used in "Die Entdeckung" for the anti-American-accent instruction added earlier this session — 34 of them (nearly half) predated that fix and never got it. All 34 are being redesigned with the current prompt builder.
[1.18.0] — 2026-07-27
Fixed
- Emotion tags barely registered on designed voices — root cause: the casting pass tags emotions in the book's own language (e.g. German "fordernd", "entschlossen"), but the instruct sentence wrapping it was hardcoded English ("Speak in a fordernd manner."), dropping a German word into an English carrier sentence — a much weaker signal than a natural sentence in one language. The template now matches the target voice's own language (inferred from its
DE_/EN_/... id prefix), applied everywhere an instruct gets built: Stage playback, Synth all, re-synthesis, Train mode, and — the one that actually matters for a finished audiobook — the export path itself, which had drifted out of sync with the others and was still building English-only instructs. - The Narrator sounded noticeably louder than designed-voice characters in a finished export — nothing in the synthesis or merge pipeline ever loudness-matched clips from different backends against each other. Every freshly-synthesized clip is now normalized to the same target used for voice reference files; a new endpoint also normalizes an already-built cache in place (pure audio processing, no resynthesis) so an existing book doesn't need a multi-hour re-synth just to fix levels.
Added
- Per-clip and whole-book-cache loudness normalization endpoints (
/api/audio/normalize-wav,/api/line-audio/{book}/normalize).
[1.17.99] — 2026-07-27
Fixed
- Designed voices could occasionally speak nonsense or repeat text mid-line, unlike cloned voices — root-caused by comparing our own request payloads against the actual Qwen3-TTS API: the app already applies a stability profile (temperature/top_p) to every Voice Clone request to keep it consistent, but Voice Design's stability profile was deliberately left empty — correct for the ONE-TIME call that designs a brand-new voice (you want fresh randomness there, or every character sounds the same), but that same empty profile was also being used for every ONGOING line of dialogue read by an already-designed voice, which needs the opposite: consistency, not randomness. Split into two separate profiles — voice creation stays unconstrained, but reading a line from an existing designed voice now gets the same stability treatment as Voice Clone. Verified live: the same line synthesized 5 times in a row went from inconsistent/occasionally-wrong to a perfect transcription match every time, with emotional instructions (tested angry vs. whisper vs. neutral) still working correctly.
- All 72 designed voices in "Die Entdeckung" were pinned to a specific seed via an automated hunt (tries a few seeds per voice, keeps whichever one round-trip-verifies correctly) rather than left on a random seed per call — the other major source of the "sometimes fine, sometimes not" inconsistency.
- Added a matching "Voice Design (reading a line)" params field in Settings, alongside the existing "Voice Design (creating a new voice)" one, so the two are no longer silently sharing one config slot.
[1.17.98] — 2026-07-26
Added
- Voice round-trip verification: synthesizes a voice's own reference line (or a language-appropriate default), transcribes the result back with Whisper, and compares it word-for-word to the original text. A duration/wpm-only benchmark can't tell "read the line correctly" from "repeated it twice" or "said something unrelated" — both can produce a perfectly normal-looking duration and pass every prior check; this actually checks the words. New "Verify (STT)" button in the Voice Library toolbar runs it over the active/selected voices and lists exactly which ones failed and what they said instead.
- The same round-trip check now runs automatically as part of every voice design/redesign attempt — a low-scoring attempt is rejected and retried just like a clipped or implausible-wpm one, so a bad design no longer needs to be caught by ear after the fact.
[1.17.97] — 2026-07-26
Fixed
- A designed voice (no reference WAV) failed outright — every time, no retry possible — whenever a bulk action picked the wrong backend for it. Root cause found in three places at once: "Precompute" and "Batch seeds" both used one globally-selected backend for every voice in a batch, and the per-voice Seed Finder didn't offer Voice Design as an option at all — so a selection that was entirely designed voices (like this book's 24-character redesign) got "Precomputed 0 embedding(s), 72 skipped/failed" and a batch-seed run that failed every single job. Every synthesis path in the app (Stage playback, Synth all, Audiobook export, Precompute, Batch seeds, Seed Finder) now resolves the correct backend per voice automatically — a designed voice always routes to Voice Design regardless of what's globally selected; every other voice still respects it.
- Added a "Voice Design" option to the per-voice Seed Finder's backend selector (it only offered Voice Clone/Streaming before), auto-selected when opening a designed voice.
[1.17.96] — 2026-07-26
Fixed
- A bulk synth/export job (Synth all, Audiobook) could lose lines outright to "Failed to fetch" with zero retry — confirmed live: a ~2000-line export lost 346 lines this way. Same root cause as the earlier voice-design fix: a GPU-contended TTS backend container can be mid-restart for a handful of seconds, and a job hitting the TTS endpoint hundreds of times in a row will reliably catch that window at least once. The core
fetchTtsPreviewBlobhelper (used by every synthesis path in the app) now retries a couple of times with a short backoff on a raw connection failure before giving up — a real HTTP error response is still surfaced immediately, only a connection that never got a response at all is retried.
[1.17.95] — 2026-07-26
Fixed
- Reassigning a character's voice in the Library/Studio Voices tab didn't reach an already-open Stage session for that book — the export kept quietly using the OLD voice, and its cached audio for that character's lines never got invalidated, which is why a re-export right after changing voices could finish suspiciously fast: it wasn't skipping work because nothing needed to change, it was skipping work it should have redone. A rehearsal loads its cast once from its own saved record, and an explicit prior voice always takes precedence over a fresher one from the shared roster — by design, so casting choices aren't clobbered — but that meant a later voice change had no path back in at all. Fixed: saving a character's voice now updates a matching, currently-open rehearsal's cast immediately and marks that speaker's already-synthesized lines stale, so the next synth/export picks up the new voice instead of reusing old audio.
[1.17.94] — 2026-07-26
Fixed
- Voices designed for sparse/generic cast entries ("Frau", "Mann", "Geliebten", "Kissen"...) could come out with the wrong gender and read an English placeholder sentence, even in an all-German book. Root cause: these are minor entries the casting pass extracted from a plain noun in the text rather than a real named character, so their sheet never gets a
genderfilled in and has no descriptive text to detect a language from — the design code was defaulting straight to English + neutral gender instead of falling back to the book's own already-known language. Fixed: the reference line now falls back to the book's resolved language (same lookup already used for the voice-design instructions) instead of English, and a handful of unambiguous German nouns ("Frau", "Mann", "Junge", "Mädchen", "Herr", "Dame") get their obvious gender when the sheet's own field is empty.
[1.17.93] — 2026-07-26
Changed
- The voice-preview play button on each character's row/card now plays that voice's own stored audio file instantly instead of re-synthesizing a sample through the TTS engine on every click. My Voices already has a fast "play original recording" button for exactly this; the character-table button was doing the slow, GPU-heavy thing instead for no benefit — you're just trying to confirm which voice a character has, and the exact audio already sits on disk.
[1.17.92] — 2026-07-26
Fixed
- "Voice design failed: Failed to fetch" could hit almost any character when designing several back-to-back — root-caused live from the server's own access log: a redesign that changes a voice's reference audio schedules a TTS backend restart so the change actually takes effect, and that restart was firing immediately after every single character (each one taking ~10-30s to come back). Clicking "design" on the next character while that restart was still in flight sent its request straight into a dead backend, failing outright with no retry. Fixed two ways: (1) individual (non-bulk) redesigns now coalesce rapid back-to-back restarts into one, firing a few seconds after the last click instead of after every single one; (2) the design/save calls themselves now retry a couple of times with a short backoff on a raw connection failure before giving up, since the window is normally only a few seconds wide.
[1.17.91] — 2026-07-26
Fixed
- A voice (or image prompt) saved for one character could silently land on a completely different character instead — confirmed live for "Marcian": every attempt to design/save his voice actually overwrote "Alrik von Blautann"'s record, because Alrik's own LLM-extracted
aliasesfield happened to literally list "Marcian" as one of his alternate names (an extraction slip). The save path re-derives which character record to write to by scanning all characters for a shared alias/name token — meant for deduping a freshly-extracted sheet at casting time, but reused here too, so one stray shared token silently redirected the write to the wrong character with no error. Fixed by having the voice/image-prompt save paths point at the exact character record they already have in hand (its own id) instead of re-guessing by alias every time.
[1.17.90] — 2026-07-26
Fixed
- Voice design could fail outright ("produced broken audio after 3 attempts") when the real cause was an unrelated backend being briefly unreachable, not bad audio. Root-caused live for character "Marcian": the post-design quality check benchmarks each attempt against the voice-clone TTS backend, which was stuck in a GPU-out-of-memory restart loop (fighting the LLM engine for shared GPU memory) — every benchmark call got a flat "Connection refused"/"Connection reset", which the check then treated exactly like a genuinely corrupted recording, burning all 3 attempts and leaving the character with no voice. Now a connectivity failure against the benchmark backend is treated as "couldn't verify" rather than "verified broken" — the design is accepted (a real defect still gets caught and rejected normally whenever the benchmark backend actually responds).
[1.17.89] — 2026-07-25
Added
- "Design a voice" now opens an inline, editable prompt popup instead of navigating away to the Design a Voice page. Clicking the design/redesign icon on a character (card, table row, or detail modal) shows the same saved/built voice-design prompt in a text box right there, with a single "Generate voice" button — edit the description and regenerate without ever leaving the Studio/Library screen, and without needing a way back afterward since you never left.
[1.17.88] — 2026-07-25
Fixed
- The voice picker opened by the "Auswahl" button in the Voices table (and in the character detail modal) could appear far away from the button that opened it — both passed the entire row/card or modal box as the anchor element instead of the button actually clicked, so the picker's position (measured via that element's own bounding box) landed wherever that much bigger container happened to start or end rather than next to "Auswahl". Confirmed live: fixed to anchor to the actual clicked button — the popup now opens directly below/beside it as expected.
- That same picker only ever showed the first 60 voices, silently hiding the rest of a larger library unless you already knew to search by name — confirmed as the reported "not all the voices I have" (154 enabled voices, only 60 ever shown). Raised to 500 (a sane upper bound, not a real-world limit) so a realistic library shows in full.
[1.17.86] — 2026-07-25
Added
- The Stage page now asks the server which lines already have cached audio on disk, in one batch request, and lights up their "pre-synthesized" dots accordingly. Previously every dot only ever reflected
rehState.synthCache— this browser tab's own memory, empty on every fresh page load — so a script that was fully "Synth all"-ed and correctly persisted to disk in an earlier session still looked completely unsynthesized after a reload, with nothing indicating the cached audio was actually right there. Confirmed live: after a reload, lines with an on-disk cache file correctly show their dot again without downloading any audio up front (that still only happens lazily, right when a line is about to play). - "Exports" button on the Stage toolbar — browse and re-download audiobook chapters already exported for the current book (via the server-side saving added in 1.17.85) without re-running the export. The export-results panel also gained a "Download all (.zip)" link when there's more than one chapter file.
[1.17.85] — 2026-07-25
Added
- "Clean cache" button on the Stage toolbar — scans the current script for every cache key its lines would actually use right now, and deletes anything else cached on disk for this book. Needed because editing a paragraph never deletes its old cached file (the write path only ever knows the new content's hash, not whatever it used to hash to before the edit) — this is the cleanup pass for that dead weight, run whenever you want rather than automatically. Verified live: cleaned up 32 orphaned files after a round of edits.
- "Audiobook" export now also saves each chapter server-side and shows a results panel with real download links, not just a browser download that lands wherever your browser settings put it with no record in the app of where it went. The panel also names the exact chapter files so you know what actually came out of the export.
Changed
- The Stage/Perform & Export page is noticeably wider (794px → 1180px) — it was simulating a literal A4-page width regardless of screen size, wasting a lot of horizontal space on wide monitors compared to the Characters view's fuller-width text column.
[1.17.84] — 2026-07-25
Added
- Every line synthesized via "Synth all", re-synthesizing a stale line, or just playing a line individually now persists to disk, keyed by a hash of exactly what determines its sound (text + voice + tone), in a folder named after the book — not just kept in the browser tab's memory like before. Editing a paragraph changes its hash, so the edited version simply never matches the old cached file and gets synthesized fresh; an untouched paragraph keeps reusing its file indefinitely, even across a page reload or a totally new browser session. This is what makes playback "flawless and fast" with no GPU wait between paragraphs once a book has been synthesized at least once — confirmed live: a second play of an already-cached line hits the new
/api/line-audioendpoint (a plain 200) instead of/api/tts-preview(the actual TTS call) at all. The full "Audiobook" export also checks this cache before synthesizing, on top of the in-memory reuse added in 1.17.79.
Fixed
- "PDF pages" mode never actually produced separate pages — the whole book rendered as one continuous page, identically to "Scroll" mode, on every book regardless of how many real page breaks its source document had. Root cause:
parseScript's page-break detection tested the line AFTER.trim()— and.trim()strips\f(form feed) as whitespace along with everything else, so the exact marker the check was looking for was always gone by the time the check ran. A real book with 232 page marks in its source text was producing zero pagebreak lines after parsing. Fixed to check the original, untrimmed line. "PDF pages" (break at the document's own real pages) is now also the default mode instead of "A4 pages" (break purely by content height, ignoring the source document's pages entirely).
[1.17.82] — 2026-07-25
Changed
- Auto-designed voices now explicitly steer away from an American English accent by default. Gender and language were already correctly sourced from the character's own sheet and the book's resolved language respectively (confirmed in the actual
/api/voice-designcall, not guessed per character) — but nothing ever told the model what accent to actually use, and its default leans American-English regardless of target language, a recurring complaint even on non-English books. The instruct text now explicitly names the accent: an authentic native accent for the book's own language when it isn't English, or a neutral British/international English accent when it is — never American. Applied both to the automated bulk "Auto-design voices" path and the LLM-authoredvoice_design_promptfield used by the manual per-character Design flow.
Fixed
- Clicking a line's play button while a previous, not-yet-synthesized line was still loading could let that stale synthesis cut in and start playing anyway — on top of, or right over, the line actually requested, with no clean way to stop just the stray one. The guard after awaiting a fresh TTS synthesis only checked a bare
rehState.playingboolean, which the new click's own stop-then-start had already flipped back totrueby the time the stale continuation resumed. Now checks that the line index itself hasn't changed since this specific playback was started, not just that something is playing. - The narrator paragraph play button (added in 1.17.74) showed two overlapping play icons on one small button — it shared a CSS class with dialogue's own avatar-badge play button, which layers a small badge icon on top via
::after, on top of this button's own separate icon. Narrator play buttons are now styled as that same badge directly instead of stacking both. - That narrator play button also never visually indicated a line was playing — same static play icon the whole time, no way to tell that clicking it again would stop it. It now swaps to a stop icon while its own line is the one active.
- The Stage sidebar's character names were always shown in shouting-case (e.g. "ALRIK VON BLAUTANN") — it was rendering the raw speaker key straight out of script parsing (which follows all-caps screenplay convention for speaker tags) verbatim, instead of the properly-cased name already sitting in the Library. Now prefers the Library's own casing, falling back to a simple per-word title-case for the rarer speaker key with no Library match, instead of shouting-case either way. The per-character colored name text (independent of this) has also been removed — names use the normal (theme) text color again, with the color cue staying on the avatar/border instead.
[1.17.79] — 2026-07-25
Added
- Shift-click range select in the Voice Library table — click one row's checkbox, then shift-click another, and everything in between gets selected/deselected to match, the same convention as a file manager. Checking dozens of rows one at a time for a bulk action (Delete, Set tag, Benchmark, …) was the alternative.
Fixed
- Exporting a full audiobook re-synthesized every single line from scratch via TTS, even ones already pre-synthesized by "Synth all" — silently redoing work that was already done, easily 20+ minutes for a real novel with no visible sign anything had gone wrong. Confirmed as the actual explanation behind "clicked Audiobook and got no file": it wasn't stuck or failing, it just had a lot of unnecessary work left to do. Already-synthesized, non-stale lines are now reused directly from the Stage's own synth cache.
- The Stage page-view button (A4 pages / Scroll / PDF pages) labeled itself with whatever mode was already active, so it read as a passive status indicator rather than something clickable — confirmed live as genuine confusion: stuck in "Scroll" (one continuous page, no page breaks) with a button that said "Scroll" and nothing hinting that clicking it would do anything. It now shows what clicking it switches TO, the normal convention for a cycle button, with the current mode moved into the tooltip.
[1.17.77] — 2026-07-24
Fixed
- Bulk "Auto-design voices" (and Assign/Generate images/Fix wrong-language voices) marked every just-created voice as "deleted from the Library — please reassign" the instant the run finished, even though the voices were sitting right there. These bulk actions re-render the character table immediately after finishing, but never refreshed
window._voices(the in-memory voice list, last loaded whenever the Voice Library page itself was visited) — so a voice created moments ago during the SAME run was judged against a stale snapshot that didn't know it existed yet, and got flagged as missing. Confirmed live: after deleting a batch of bad-accent voices and re-running Auto-design, every freshly-designed replacement showed the "missing voice" warning icon immediately, even though/api/voicesalready listed them correctly — a fresh page reload alone was enough to make the warnings disappear, confirming this was a stale-cache display bug, not a real data problem. The voice library now reloads right before that final re-render. - Reusing a voice from "the same character elsewhere" (the fast path bulk designs use to keep a recurring character's voice consistent across a series) could hand out a voice that had since been deleted from the Library, if some OTHER character record still referenced it and was never cleaned up. Now checks the voice still actually exists before treating it as reusable, same as the Library's own "missing voice" indicator already does everywhere else.
[1.17.75] — 2026-07-24
Fixed
- The character-sheets progress box was back to leaving a huge empty area below its content, this time regardless of how much had actually been generated. The previous fix (making the box shrink-to-content via
flex: 0 1 auto) turned out to fight its own direct child,.cs-progress-layout, which is itselfflex: 1(meant to fill whatever height the box has) — that made the box's "content size" circular, since it had no real minimum to shrink to, and it silently kept resolving to the full 90vh cap no matter how little content existed yet. Confirmed live: a result with only 8 characters found still stretched the box to the exact viewport-height ceiling, leaving a large empty gap below the small amount of real content. Reverted the box itself toflex: 1(fill the section, no gap below it — that was never actually the wrong part) and left the short-content problem to the preview/output split fixed earlier in 1.17.66, which doesn't have this circular-sizing conflict. Verified live at a realistic tall viewport: a short 8-character result no longer stretches the box, and a full 72-character list still scrolls correctly with no regression.
[1.17.74] — 2026-07-24
Added
- Narrator paragraphs on the Stage page now have their own play button, matching dialogue lines — previously only dialogue lines (
.reh-block) had a play/pause control; narration paragraphs (.reh-action-block) had an edit button and a pre-synthesized dot but no way to play them individually. Deliberately kept plain rather than reusing dialogue's boxed/avatar/name-row treatment — just a small icon before the paragraph text, so narration keeps reading like narration instead of being visually pulled into a quote-like card. Reuses the same click wiring as the dialogue play button (.reh-line-play-avatar), so it needs no new playback logic. Verified live: clicking it correctly sets the active line and starts playback, same as a dialogue line's own button.
[1.17.73] — 2026-07-24
Fixed
- The Stage/Perform & Export character sidebar never showed portrait thumbnails, even though the exact same characters show real photos everywhere else (Voices phase, Library). Root cause:
renderCastList(the "Who's playing which character?" panel) andrenderCastStrip(the Stage sidebar) share ONE cached fetch of the book's character records, guarded by a single "already fetching this book" flag — but onlyrenderCastListre-rendered itself when that shared fetch resolved. Entering Perform & Export borrows both panels at once, andrenderCastList's guard check usually won the race, silently claiming the fetch and leavingrenderCastStripwith no way to know the data (with portraits) had actually arrived — it stayed on plain colored-letter dots forever, even though the cache had genuinely finished loading moments later. Both renderers now re-run whenever the shared fetch resolves, regardless of which one triggered it. Verified live: a 72-character sidebar went from 0 portraits to 29 real portraits rendering automatically on first entry, no manual action needed.
Changed
- Character thumbnails in cast/roster lists (Stage sidebar, casting sidebar, recast picker) now show a 2px border in the character's own cast color, matching the color-coding the plain letter-dot fallback already had — a real photo no longer loses that at-a-glance color identity in a long list.
[1.17.72] — 2026-07-24
Fixed
- Batch-benchmarking a group of designed voices reported every single one as an error, even though the voice_design engine itself was reachable the whole time. Root cause was server-side this time, in
core/tts_helpers.py's_tts_benchmark_request: it always built a generic voice_clone-style request against the one fixed TTS URL, with no awareness that a designed voice (no reference WAV) needs a completely different endpoint and request shape — the same one/api/tts-preview'sbackend=='voice_design'path already uses successfully. Confirmed live: benchmarking a real designed voice ("EN_M_Junker") directly via/api/voices/benchmarkfailed with the voice_clone connection error until this fix, then succeeded (rtf 0.8, ok) once it routed through the correct engine._benchmark_voicenow checks each voice's own metadata (origin/reference-audio presence) before benchmarking and dispatches accordingly — genuinely cloned voices are unaffected and still correctly report the real voice_clone-backend outage rather than silently succeeding.
[1.17.71] — 2026-07-24
Fixed
- The Voice Library table's own "Generate and play TTS preview" button had the same wrong-backend bug as 1.17.69's character-preview fix, in a different file. It always synthesized through whatever engine the page-wide "Library TTS backend" selector happened to be set to — a deliberate override meant for bulk actions like Benchmark/Precompute where testing everything against one chosen engine on purpose makes sense — but applying that same override to a single voice's own row meant a designed voice failed outright unless the user had separately remembered to flip that selector to Voice Design first. Confirmed live: with the selector left on its voice_clone default, "EN_M_Junker" (a designed voice) failed with a voice_clone connection error even though Voice Design was reachable the whole time. Cloned voices still honor the selector (several backends can legitimately play a reference-WAV voice), but a designed voice's preview now always uses Voice Design regardless of what the selector is set to. Verified live: the same voice now plays successfully via "8021 Voice Design".
[1.17.70] — 2026-07-23
Fixed
- Sparse/minor characters in an all-German (or any non-English) book could get auto-designed as English voices — confirmed live on a real book: "Junker", "Kroah", "Leonardo" and a few other minor characters got British-flagged EN voices while every other character in the same batch correctly got DE. Root cause:
detectLang()(utils.js) is documented to return''when there's too little text to be confident, and every one of its callers already treats a falsy result as "couldn't tell" and falls back accordingly — but the function itself violated its own contract and silently returned the literal string'English'instead. Since that's truthy,_resolveBookLang()(library-characters.js) — which exists specifically to catch exactly this case via a per-book majority-vote fallback across a character's siblings — treated the bogus 'English' guess as a confident, final answer and never got to use its own fallback.detectLang()now actually returns''on weak signal as documented. Voices already designed with the wrong language before this fix keep their existing (mistagged) id and aren't renamed automatically — only new auto-designed voices from here on are affected.
[1.17.69] — 2026-07-23
Fixed
- A character's voice preview (▶ in the Cast table/profile) always tried to play through the voice_clone engine, even for a designed voice that has no reference WAV to clone from at all. This "worked" only by coincidence whenever the voice_clone backend happened to be reachable, and broke for every voice — cloned or designed — the moment it wasn't, even though the designed voice's actual engine (voice_design) was up the whole time. Confirmed live: with the voice_clone backend down, previewing a designed voice ("Zerwas") failed identically to a real cloned voice ("Narrator"), even though voice_design itself responded fine directly. Now resolves the correct engine per voice (
origin === 'designed'or no reference audio → voice_design, otherwise voice_clone) instead of hardcoding one for every voice. Verified live: the designed voice's preview now plays successfully; the genuinely cloned voice still correctly fails while its own backend is down, with no change needed there since that's a real infrastructure outage, not a code bug.
[1.17.68] — 2026-07-23
Changed
- Concept art is now a full-width banner below the header instead of a small 110px thumbnail squeezed in next to the avatar — too small to make out any real detail in a multi-pose design sheet. It now gets its own full-width section right under the header, sized up to 70vh, with the "Generate"/"Regenerate" button moved to a label row above it.
- The Perform & Export (Stage) character sidebar now shares the exact same search/sort styling as the Characters phase's sidebar. Both already reused the same
.ab-cv-sidecontainer, row markup, and collapse-to-avatars behavior, but Stage's search box and sort dropdown used their own separate, slightly different-looking CSS (.reh-cast-side-toolswith bare, unclassedinput/select) instead of the shared.ab-cv-side-search/.ab-cv-side-sortclasses — a leftover inconsistency from when the two sidebars were built at different times. Switched Stage's markup to the shared classes and removed the now-unused duplicate CSS. Verified live: collapse/expand still works correctly on both.
[1.17.66] — 2026-07-23
Added
- Concept art is now generated automatically for every character, and shown right in their profile. Previously, the Concept Art Prompt (an auto-generated NPC design-sheet description) only ever produced text sitting in the Generation Prompts section — turning it into an actual image required noticing it, scrolling down, and clicking "Generate Concept Art" per character. The same background pass that already fills in
silly_tavern_prompt/concept_art_promptfor a freshly cast book now also generates the image itself for every character with a prompt, right after casting finishes — no manual step needed. The result is displayed prominently in the character's profile page, right next to the avatar in the header, with click-to-enlarge and a "Neu generieren" button for regenerating it by hand at any time. Verified live: a real character ("Admiral Sanin") went from an empty "Kein Konzeptbild" placeholder to a generated design-sheet image shown in the header, openable full-size.
[1.17.65] — 2026-07-23
Fixed
- The voice picker ("Auswählen" on a character card) could permanently show "No voices found" even though the voice library had loaded moments later — it snapshotted
window._voicesonce when opened and never looked again, so opening it before the library's background fetch finished (most likely reachable via Studio's Voices phase, which borrows the character grid without itself triggering a voice-library load) left it stuck empty for the rest of that popup's life, with no way to recover short of closing and reopening it. It now kicks off (or reuses) the voice-library load itself when it opens empty, and re-renders the list once that resolves. Verified live: a picker opened against a deliberately delayed/empty voice list showed "No voices found" and then correctly populated with all voices once the load completed, with no user action needed.
[1.17.64] — 2026-07-23
Fixed
- 1.17.60's
flex: 1fix for the below-card gap created its own empty-space bug: with a real, populated character list next to a short/not-yet-filled character card (e.g. right after selecting a character whose sheet hasn't generated much yet), the sidebar's genuine need for height (to show many rows with a working scrollbar) stretched the preview panel to match via the grid'salign-items: stretch, leaving a large empty box below the short card's actual content — confirmed live with a 72-character sidebar next to a two-line preview (296px box, only 63px of real content). Root cause:.cs-progress-previewhadflex: 1, forcing it to grow and fill whatever height its sibling column demanded, even when it had nothing to show there. Changed the preview panel to shrink-to-its-content (floored at its existing 220px minimum, so it doesn't look collapsed when truly empty) and let.cs-progress-output(the passage/live-output pane, which already scrolls its own content and existed for the "growing" case anyway) absorb any leftover column height instead. Also confirmed (separately, via the same live test) that.cs-progress-box-big's own height needed an explicitheight: autooverride — a more specific scoped rule that only setflex: 0 1 autowas silently losing to an unscoped, unrelated rule's explicitheightelsewhere in the stylesheet, since CSS cascade resolves per-property, not per-rule. Verified live: preview panel now matches its actual content instead of stretching, while a 72-character sidebar list still scrolls correctly (2526px scrollHeight vs. 454px clientHeight).
[1.17.61] — 2026-07-23
Fixed
- Clicking a character in the sidebar no longer scrolled to their actual first line — a real regression from the earlier
content-visibility: autoperformance fix (1.17.49). The selection/data logic itself was correct (confirmed live: the right dialogue row was genuinely focused), but off-screen pages' heights are only an ESTIMATE (contain-intrinsic-size) until the browser actually measures them, so a singlescrollIntoView()computed against a target many pages away could land wherever the sum of all those estimation errors put it — confirmed live at over 4000px off-screen. EveryscrollIntoViewcall in the casting feed now does an instant rough pass followed by a corrected smooth pass once the browser has actually laid out whatever came into view, without giving up the original performance win. Verified live: the target row went from ~4098px off-screen to correctly positioned inside the visible feed.
[1.17.60] — 2026-07-23
Fixed
- Still wasted empty space below the character-sheets card after 1.17.59 — that fix matched the two internal columns to each other, but the whole card was still capped at a fixed viewport-relative height, while its section (
#s-reader/#s-caststudio) is a fixed, full-height flex column regardless (needed for other content like the PDF reader). Any time the card's own capped height came in under the section's real height, the leftover section space showed up as dead area below the whole card, not just between its columns. Switched toflex: 1(the same pattern#reader-main-viewalready uses under these same two sections) so the card actually claims whatever height the section provides instead of guessing a fixed number. Verified live: section/panel/box all measure identically now (747px), with zero leftover gap.
[1.17.59] — 2026-07-23
Fixed
- The 1.17.56 sidebar-scrolling fix introduced a new mismatch: a large empty gap below the shorter of the two columns. Giving the sidebar a fixed height cap independent of the main passage/output column meant the two no longer matched — when the passage side was short (e.g. still on passage 1, or mid network-error-retry) while the sidebar's cap stayed constant, the shorter column left dead space below it. Replaced the fixed cap with the same bounded, viewport-relative height the floating-overlay version already uses, so both columns stretch to match the SAME real height via the grid and scroll independently within it — no constant on either side to drift out of sync with the other. Also removed an explicit
height: 100%on the sidebar that created a circular sizing reference against the grid's own auto-sized row (a child asking for "100% of a row whose height is itself content-derived" resolves back to the child's own content size, not the row's actual height) — grid'salign-items: stretchalready gives it a correct, definite height with no percentage math involved. Verified live: both columns now measure identically (641px each) instead of one leaving unused space.
[1.17.57] — 2026-07-23
Fixed
- Every line in the casting feed could get silently rendered twice — the same segment appearing as two separate (but identical, same underlying object) DOM rows. Confirmed live: a character's line count in the top per-character navigator ("bar-pos") read exactly double what the sidebar correctly showed, and clicking a character in the sidebar no longer scrolled to their actual first line, since the duplicate rows threw off which DOM node was really "first" in reading order. Root cause: the feed's chunked redraw had no protection against two overlapping calls — each one's own in-flight batch loop kept running independently of the other's
feed.innerHTML = '', so two legitimate (non-recursive) triggers close together both ended up appending their own full copy of every row. A generation token now lets an in-flight redraw notice it's been superseded by a newer one and stop instead of racing it. Verified live: a 1598-segment feed went from 1500 (partially duplicated, inconsistent) rows to exactly 1598, with a 1:1 match between DOM rows and segments for every character checked.
[1.17.56] — 2026-07-23
Fixed
- The character-sheets generation progress view's own "Characters found" sidebar had the same no-scrollbar symptom as 1.17.55, but a different root cause. When this panel is embedded inline in the page (Read Aloud / Studio) rather than shown as a floating overlay, its outer box deliberately gets
height: autoso it grows naturally with the page — but that removes the one bounded ancestor the sidebar's whole flex/overflow chain needs to scroll internally. Gave the sidebar its own independent height ceiling for this specific context, regardless of what the main passage/output column is doing. Verified live with a real 72-character list: went from zero usable height to scrolling correctly within a bounded ~640px box.
[1.17.55] — 2026-07-23
Fixed
- The "Characters found" sidebar on the casting view had no scrollbar, leaving a large mismatched empty gap in the feed column next to it.
.ab-cv-chars(the scrolling list itself) was missingflex: 1— without it, it just sized to its own content like any other block element, so its ownoverflow-y: autonever had a bounded height to actually scroll within; the parent'soverflow: hiddenjust silently clipped it with no scrollbar shown, while the grid row stretched to match the unbounded content anyway. Verified live: a 72-character list went from growing past its container (scrollHeight 3452px vs. a 404px box, no scrollbar) to scrolling correctly within it.
[1.17.54] — 2026-07-23
Added
- Concept art can now actually be generated as an image, not just text. The "Character Concept Art Prompt" box previously had no image action at all (only Copy/Regenerate for the prompt text itself) — added a "Generate Concept Art" button that calls the same image-generation pipeline as the profile portrait, storing the result alongside the prompt (never overwriting the character's main portrait) and showing it inline in the box.
Changed
- Rewrote the concept-art prompt to follow a structured Task/Subject/Context/Style/Composition/Lighting/Constraints/Output pattern (adapted from a user-supplied reference), producing a proper NPC/character reference sheet: full-body front/side/back views plus an isolated callout row for the character's own established props/costume pieces (only when the profile actually has any — no invented gear), with explicit genre/era grounding and an original-character/no-copyright disclaimer. Verified live against a real character — produces exact prop counts and setting-appropriate detail instead of the previous generic single-sentence version.
[1.17.53] — 2026-07-23
Changed
- Rewrote the "SillyTavern Character Prompt" generation. It was only ever built as a few short labelled lines (Description/Personality/Scenario/First message/Example dialogue) via a generic instruction — now generates a full structured SillyTavern character card (Personal Information, Appearance, Personality, Likes, Dislikes, Goals, Skills, Weapons, per-occasion Outfits, gender-aware), followed by an open-ended Scenario block and a First Message, adapted from a user-supplied prompt template. Since the character's full profile is already known from the book (unlike the original template, written for building a card from scratch via internet research), the model is told to use ONLY that profile instead of searching fandom/wikipedia, marking anything genuinely inferred with a trailing
*. Bumped this field's own token budget (1536 → 3000) since a full card no longer fits the old ceiling.
Known limitation
- The local model sometimes stops right after the Scenario block and skips the First Message despite an explicit instruction to include it — a model-following limitation, not a formatting bug. Worth another look if it's consistently a problem.
[1.17.52] — 2026-07-22
Fixed
- Found and fixed a real infinite-recursion bug introduced in 1.17.45, confirmed live via the browser's own renderer process sitting pegged at steady high CPU (not idle — a genuine runaway loop, not a hang):
audiobookCastView()callsnavReaderView('cast')(an alias forshowReaderView) as part of its own normal setup — which is exactly the function 1.17.45 wrapped to keep Studio's sub-tabs in sync. That wrapper called_stuShowCastView('identify'), which callsaudiobookOpenCastView(), which callsaudiobookCastView()again, which callsnavReaderView('cast')again — forever. This reliably reproduced entering Studio's Characters tab on a book with an existing cast. Fixed with two guards: never re-enter the wrapper while already inside a call it triggered, and skip entirely once Studio's sub-tab already matches the requested view (the overwhelmingly common case this was needlessly re-triggering on every call). Some, but likely not all, of the freezes reported since 1.17.45 were probably this.
[1.17.51] — 2026-07-22
Fixed
- Extended the content-visibility fix to the character-cards grid (
.lib-char-card, shared by Library's Cast page and Studio's Characters tab — same borrowed elements, not separate code). A production with dozens of characters, each carrying a generated portrait image, stayed fully laid out and painted at once with no virtualization — the same category of issue just fixed for the casting view, likely also in play during bulk profile/portrait/voice generation across a large cast. Same proven technique, same guarantee: off-screen cards stop costing layout/paint, nothing else changes.
[1.17.50] — 2026-07-22
Fixed
- Reproduced and fixed the actual cause of the recurring tab freeze/crash, verified live: restoring a fully-cast, 235-page book kept all ~40,000 DOM nodes (every narration/dialogue row for the whole book, by design — so the full cast stays available for review) fully laid out and painted at once, even the ~99% currently scrolled out of view. Confirmed measurably: before this fix, scrolling the restored view took real, non-trivial time; after, scrolling to the very bottom or top of a 1598-segment cast is instant (0ms).
content-visibility: auto(the same technique already used elsewhere in this app for other large lists) now lets the browser skip layout/paint work for off-screen page groups entirely — the data and every existing feature (search, click-to-edit, page navigation) are completely unaffected, since the nodes are still there, just not actively rendered while off-screen.
[1.17.49] — 2026-07-22
Fixed
- Casting a long book could hang and crash the tab partway through (confirmed live: crashed around page 33 of a 235-page book, 1598 segments in). Two compounding O(n²) costs in the live casting view: (1) the feed's own housekeeping ran a full
querySelectorAllover every row/note/divider in the whole book-so-far on every ~80-segment batch, for a trim limit that's permanently disabled (infinite) — pure wasted work that grows with the book; (2) the autosave fired after every single chunk, re-stringifying and both localStorage-writing and network-sending the ENTIRE accumulated segments array every time, also growing with the book. Both scale with total segments cast so far, not a flat per-page cost, so the first ~30 pages felt fine and it degraded from there. The feed query is now skipped entirely since it could never do anything anyway, and autosaves are throttled to at most once every 4 seconds (always still saving on completion, so nothing is lost at the end of a run).
[1.17.48] — 2026-07-22
Fixed
- PDF zoom hang persisted even with the render timeout from 1.17.47. That confirms the hang is likely a synchronous main-thread block (e.g. PDF.js decoding an embedded image at its own native resolution before any downscaling) — nothing awaitable, including a timeout, can rescue a hang that already owns the thread before our code gets a chance to run. Since shrinking the output canvas doesn't help if the cost comes from the source image's own resolution, the safety budget is now much smaller (~2MP / 1800px per side, down from ~6MP / 3000px) so the requested render scale for an oversized page stays low enough to avoid the pathological decode in the first place, regardless of the zoom level shown in the UI.
[1.17.47] — 2026-07-22
Fixed
- The previous canvas-size clamp (1.17.46) alone wasn't enough to stop a page hanging the tab. A single oversized dimension can be a problem even under the area budget on some browsers, and — more importantly — the page render itself can still hang regardless of the requested output size (e.g. an embedded image codec that decodes at native resolution before any downscaling happens). Added a hard per-side cap alongside the area budget, and — the actual guarantee this time — a timeout around the render call itself: a page that doesn't finish rendering within 15s is cancelled and left for a later retry (e.g. scrolling away and back) instead of hanging the tab indefinitely.
[1.17.46] — 2026-07-22
Fixed
- Zooming a PDF page in (a cover page in particular) could hang and then crash the entire browser tab (Chromium "Aw, Snap! SIGTRAP"). Page canvases were sized directly from page size × zoom with no upper bound — a page with an unusually large intrinsic size (some scanned/cover pages) at ~400% zoom could demand a multi-hundred-megabyte backing buffer. The actual rendered resolution is now capped to a safe pixel budget regardless of zoom or page size; the canvas's on-screen (CSS) size still matches the requested zoom exactly, so a single oversized page just renders a touch softer instead of crashing the tab.
[1.17.45] — 2026-07-22
Fixed
- "New recast (discard & rebuild all)" (and other actions that call
showReaderViewdirectly) left Studio's Characters tab completely blank, with the LLM pass clearly running in the background (GPU busy) but nothing visible at all. Studio shows/hides the borrowed casting panels one level up via its own slot visibility, in lockstep with its "Identify Characters"/"Cast Characters" sub-tabs — butshowReaderView('cast'|'chars')toggles.hiddendirectly on the panels themselves, one level down, with no awareness of Studio's slots. If the two disagreed (e.g. recast was triggered while on the wrong sub-tab), the panel actually holding the live progress UI ended up hidden inside its slot while the other, now-empty panel's slot was the visible one — a blank screen with real work happening underneath.showReaderViewnow keeps Studio's own tab UI in sync whenever this happens.
[1.17.44] — 2026-07-22
Fixed
- Speaker attribution left too many lines as "Unknown" on cues a human reader would catch instantly: self-introductions ("Man nennt mich Andra"), and idiomatic narration that doesn't use a literal speech verb ("Marcian fand als erster seine Stimme wieder", "ihre ersten Worte waren..."). The LLM attribution prompt now has explicit rules for both (self-introduction, and a widened voice-announcement rule covering these idioms), and the deterministic code-level fallback — which already exists because the LLM alone measurably still misses ~44% of mechanical patterns even when the prompt spells them out — now catches these same patterns directly instead of only literal speech-verb tags.
[1.17.43] — 2026-07-22
Fixed
- PDF import could get permanently stuck on one page ("Reading PDF… page 224/235" that never advances). The per-page text/heading extraction loop had no timeout anywhere — a hung (not rejected, just never-settling) page render or Tesseract OCR call, most likely on an image-heavy or blank page, silently stalled the entire sequential import with no error and no way to recover short of reloading. Page render, heading OCR, and plain text extraction now all race against a timeout and skip to the next page (logging a warning) instead of hanging forever.
[1.17.42] — 2026-07-21
Fixed
- "Voice design failed... produced broken audio after 3 attempts" fired on perfectly good voices. The retry logic's "bad" check included the benchmark's
realtime_okflag, which also goes false whenever the designed voice's own reference audio (built from the character's sample text — often a real book quote) simply runs longer than 25 seconds, a length/performance advisory with nothing to do with audio corruption. Since sample length barely changes between retries, this deterministically failed all 3 attempts for any character with a longer line, wasting 3 generations and then refusing to assign any voice at all. Only real defects (synthesis error, output clipped at the max-duration guard, implausible wpm) count as "bad" now.
[1.17.41] — 2026-07-21
Added
- Per-book context (genre, setting, era, language), editable via a new "Context" button on each production — e.g. "High fantasy, like Lord of the Rings, medieval times, German". Feeds directly into every voice design prompt for that book, and is now the authoritative language source for language-matching/mismatch checks, instead of guessing from one character's own (often sparse) sheet text.
Fixed
- Bulk "Auto-design voices" silently did nothing for characters whose name is a known alias of another character (e.g. "Garbaz" as an alias of "Arthag") — the save step's identity-matching redirected the update onto the alias's canonical record instead of the character actually selected, so the selected row never got a voice while a seemingly unrelated character's voice silently changed instead. This is a known limitation of the alias-based identity system, not yet fixed at the root — but is now at least understood; a real fix needs to distinguish "explicit per-card action" writes from freeform sheet-merge writes.
- Sparse/minor characters ("Bote", "Frau", "Mann", generic crowd roles) with no descriptive sheet text kept getting voices designed in English by default even in all-German books, because the per-character language detection this relied on has nothing to detect from on a near-empty sheet. Now falls back to a majority vote across the character's own book siblings, and — with the new book context above — to an explicit setting first.
- A voice that failed the benchmark on all 3 retry attempts was still committed as the character's final voice anyway (with only an easy-to-miss toast), which is exactly how several visibly-broken voices (0.02x factor, 1875 wpm) ended up in the library despite the retry logic added in 1.17.36. It now fails the character outright instead — leaving whatever voice it had before (or none) untouched — rather than silently swapping in audio already proven broken.
[1.17.40] — 2026-07-21
Fixed
- Deleted voices could reappear on their own. The background voice-index rebuild (a full disk rescan that can take a noticeable while over 100+ voices) replaces the entire index with what it found once it finishes — if a voice was deleted while that scan was still in progress, its row was still sitting in the scan's snapshot from before the delete, so the rebuild resurrected it the moment it finished. This got much easier to hit now that voice-design retries poll the backend far more often. The scan now re-checks each file still exists immediately before writing, closing the window down to next to nothing.
[1.17.39] — 2026-07-21
Fixed
- Restarting the TTS backend (including the new automatic post-design restart added in 1.17.36) blocked the entire app server for the whole restart duration.
/api/tts/restartand the local-container start/stop/restart endpoints called a raw blocking Docker-socket request directly inside their async route handlers instead of off-thread — with two TTS containers configured, that's up to ~20s where every other request (including a plain character list fetch) just hung. Confirmed live: switching to Studio's Voices tab during a bulk voice-design run showed "No characters yet" for a book that very much had characters, because its own character fetch got starved by an in-flight restart. All Docker socket calls in these routes now run in a worker thread instead of on the event loop. - A failed character-list fetch (of any cause) blanked out an already-populated list instead of leaving it alone.
libraryRenderCharacters()now fetches before touching the DOM, so a transient failure just leaves the current view in place (with an error toast) rather than replacing real data with an empty state.
[1.17.38] — 2026-07-21
Fixed
- Every action on the Characters/Cast page (remove voice, auto-design, delete, etc.) jumped the page back to the top. The full-list rebuild those actions trigger briefly collapses the list down to a one-line loading placeholder, which the browser responds to by clamping the scroll position back to fit — confirmed live as a scroll-to-top after every single click. The page's scroll position is now restored once the rebuilt list is back in place.
[1.17.37] — 2026-07-21
Fixed
- Deleting a broken voice from the Voice Library left every character still pointing at it, with no indication anything was wrong. Character cards, the table view, and the character detail modal now check each assigned voice id against the loaded voice list and mark it red (with an alert icon and an explanatory tooltip) when it no longer exists, instead of silently showing a dead voice id as if it were still valid.
[1.17.36] — 2026-07-21
Fixed
- Designed voices could end up permanently broken (near-silent/truncated output, or absurdly slow to render) with no automatic detection. Confirmed live across a whole book's cast: some designed voices measured impossible speech rates (6 wpm, 5625 wpm) or failed the realtime benchmark outright. Auto-design/redesign now benchmarks each generation attempt (under a disposable id, never the character's real voice) before committing it, and automatically deletes and retries up to 3 times if the result is corrupted, clipped, or outside a plausible 80–400 wpm range; if all attempts still fail, the character keeps the last attempt and a toast flags it for manual review.
- Overwriting an existing voice's audio during a redesign never flagged the TTS backend as needing a restart, unlike the equivalent "replace audio" path — so a freshly redesigned voice could silently keep serving the old cached reference audio during synthesis, with no warning shown anywhere.
/api/savenow sets the sameneeds_tts_restartflag/api/voice-replacealready did. - Restarting the TTS backend after a redesign now happens automatically instead of relying on the user to notice the warning and click "Restart TTS" themselves — once after a single redesign, once after a whole bulk batch (not per-voice, to avoid restarting a model server dozens of times in one run).
Security
- Voice reference photos could be silently and permanently overwritten with no backup, discovered after a real character-portrait image overwrote a cloned narrator's actual reference photo with zero way to recover it. Picture uploads now back up any existing picture before overwriting it (mirroring the existing audio backup pattern), so future overwrites are recoverable even when triggered by a bug.
[1.17.34] — 2026-07-21
Changed
- Voice design prompts now demand specific, differentiating detail instead of generic category labels. "Young female voice, energetic tone, clear pitch" describes a whole demographic, not a person — confirmed live as the real cause of several same-age/gender characters sounding near-identical. Both the LLM-generated prompt and the client-side fallback (for sparse profiles with none of the richer sheet fields filled in) now push for a specific timbre, pace/rhythm quirk, and character-specific emotional baseline; the fallback also gets a deterministic per-character variety injection so even the sparsest profile differs from every other same-gender character instead of using the identical generic sentence.
- "Design a voice" (single-character, manual flow) now asks before silently reusing a series voice, with a way to hear the existing voice first — confirmed live as confusing (clicking "design" expecting something new, silently getting a reused voice instead with only an after-the-fact toast). Choice of using the existing voice or designing a genuinely new one. Bulk actions are unaffected — they keep the fast, silent reuse-first behavior a 40-character run needs.
[1.17.33] — 2026-07-21
Fixed
- "Rebenchmark this one" crashed with "Cannot set properties of null (setting 'className')" from the single-voice detail view. Opening a voice in the inspector physically moves its benchmark chip element out of the row and into the inspector panel — the rebenchmark handler was still looking for it in the old (now-empty) location. Falls back to the inspector's own copy, and no longer crashes if it genuinely can't find either.
[1.17.32] — 2026-07-21
Added
- "Remove voice" button in the character table — clears the assigned voice, leaving the character unassigned (e.g. before picking or designing a replacement).
- "Fix wrong-language voices" bulk action — scans every character in a production (no selection needed) for a voice whose language doesn't match the book's own, and designs a properly-matching replacement for each. Cleans up characters assigned before the language-matching fix (1.17.27) existed, or from an older bulk run.
- "Edit voice design prompt" button — opens the Design a Voice page pre-filled with the character's Voice Design Prompt (name, gender, language, and prompt text), so it can be reviewed/adjusted before regenerating, instead of only ever getting an instant, un-editable auto-design.
- Hovering a character's profile picture now shows a large (512×512) preview near the cursor — the table/card thumbnail alone (often just 32-40px) gave no real sense of the actual portrait.
[1.17.31] — 2026-07-21
Added
- A dedicated "design a new voice" button in the character table, for when the current voice just doesn't fit — unlike "Auto" (which prefers reusing an existing/series voice first), this always generates a genuinely fresh voice, bypassing any reuse/match logic entirely.
Fixed
- The voice play button's horizontal position varied row to row depending on how long the voice name was, since it sat on the same row as the name. Moved to the second (action) row, so it lines up consistently under every row's voice name regardless of length.
[1.17.30] — 2026-07-21
Fixed
- Long voice names pushed the "Auswahl"/"Auto" buttons out of the visible column in the character table. The voice name, play button, and both action buttons were all one row in a fixed-width column — confirmed live, a longer voice id left the buttons cut off entirely. Now two stacked rows: voice name + play button on top, Auswahl/Auto underneath.
[1.17.29] — 2026-07-21
Changed
- Voice previews and voice-design samples now include a real line from the book, not just a bare greeting. The quick preview button ("Hallo, ich bin NAME") and the actual voice-design generation step now both append an actual line the character speaks (from the current audiobook's attributed dialogue, or the character sheet's own quoted sources) when one's available — much better preview of how the voice actually sounds reading the book than a name-only greeting alone.
[1.17.28] — 2026-07-21
Fixed
- "Auto-design voices" silently did nothing for a character that already had a voice. The series-reuse check (
_findVoiceFromSameCharacterElsewhere) runs before generation to avoid burning a fresh design call for a recurring character — but since it naturally finds the character's own most-recent record first, clicking design on an already-voiced character just re-saved the identical voice id and returned, never actually generating anything new. Now skipped specifically when the "reuse" would be a no-op (matches what the character already has); genuine cross-book reuse (a distinct voice this record doesn't have yet) still applies as before.
[1.17.27] — 2026-07-21
Fixed
- Auto-designed voices all sounded nearly identical regardless of character. The shared TTS "stability" defaults (fixed
seed: 0,temperature: 0.1) were applied to voice DESIGN calls too, not just voice CLONE calls — sensible for clone (consistent retakes of the same voice), backwards for design (every call should produce a different voice from a different prompt). Pinning the random draw meant the character's own prompt text became the only source of variation, and low temperature flattened even that. Voice design now uses the backend's natural per-call randomization, same as other creative-voice backends. - Existing-voice reuse/name-matching ignored language, producing accent mismatches. Confirmed live: English-designed voices got assigned to German-book characters via name-substring matching. Neither the series-reuse check nor the new name-matching check verified the voice's language (encoded only as an
EN_/DE_-style id prefix, the only place language is ever recorded) against the book's own detected language. Both now skip a same-name match in the wrong language and fall through to designing a fresh one instead. - Character portrait/profile image prompts had no genre/setting anchor, so a fantasy book's military-sounding occupations ("Admiral", "General", "Prinz") could render as modern-day imagery. Both the LLM-generated prompt and the client-side fallback now explicitly work out and state the story's genre/era, and instruct against real-world modern anachronisms unless the story is actually contemporary.
[1.17.26] — 2026-07-20
Changed
- "Auto-assign selected" now designs a bespoke voice per character instead of handing out an arbitrary same-gender pick. Previously it just filtered the whole voice library by gender and grabbed the highest-rated match, with no check that the voice had anything to do with the actual character — confirmed live: a 46-character bulk assign returned existing voices for every single one, several clearly generic. Now: reuse a voice from the same-named character elsewhere in the series (unchanged) → otherwise use a voice already in the library whose own name matches the character's (e.g. "DE_M_Zerwas" for a character named "Zerwas") → otherwise design a new voice from that character's own profile.
[1.17.25] — 2026-07-20
Fixed
- Voices phase could auto-expand the wrong book. Scoping which book's section shows expanded read
readerState.title, which is left pointing at whichever book was last opened via the Source tab — not necessarily the book whose cast is actually active. Confirmed live: navigating to Voices via "View cast" from a book 2 casting session expanded book 1's section instead. Now resolves the title from_audiobook.bookId(kept correctly in sync with the active cast session) via the server, falling back toreaderState.titleonly when no cast session is active.
[1.17.24] — 2026-07-20
Fixed
- "View cast" (Cast ⌄ menu, Characters tab) did nothing when clicked in Studio. Same root cause as the earlier "Open Script Rehearser" fix: it called
navTo('s-library')directly, silently swallowed by Studio's nav guard while Studio is the active section. Redirects to Studio's own Voices phase instead, which already shows the same cast/character roster.
[1.17.23] — 2026-07-20
Added
- Studio's phase tabs (Source/Characters/Voices/Perform & Export) didn't read as a sequential flow. The old cross-section "Previous/Next" stepper was deliberately removed from Studio in 1.17.8 (it only knew about the old sections, not Studio's own phases), but nothing took its place — the only way to move forward was clicking a tab directly, with no visible "what's next" affordance. Each tab is now numbered (1-4), and a Studio-specific Previous/Next button pair sits in the header, disabled at the first/last phase.
[1.17.22] — 2026-07-20
Fixed
- Studio's Characters tab could end up with no navigation controls visible at all. The relocated dropdown toolbar (1.17.15) only ever hid the plain "Identify Characters"/"Cast Characters" tabs once a footer was found to replace them — a later transient state (e.g. "Checking saved cast before starting a new one…") rebuilds the panel without any footer at all, and since nothing re-showed the plain tabs, confirmed live: neither the dropdown toolbar nor the plain tabs were visible, leaving no way to navigate. The plain tabs now fall back to visible any time there's genuinely no footer to relocate, not just the first time.
[1.17.21] — 2026-07-20
Fixed
- Exported audiobooks were only playable for the first few seconds, despite a correctly-sized file.
audiobookExport()synthesized every line as an independent mp3 clip and merged chapters by naively concatenating the raw mp3 byte streams (new Blob(blobs, {type:'audio/mpeg'})) — each clip carries its own frame/ID3 headers, which most players decode only the first of before stopping or glitching (confirmed live: an 85MB export that reported as 22 seconds playable). Also confirmed the per-line mp3 encoding was never given an explicit bitrate, silently falling back to ffmpeg/lame's low default (32kbps) rather than any bitrate this app actually chose. Fixed at the source: lines now synthesize as lossless WAV, merge properly per chapter via the same correct PCM-concatenation helper already used elsewhere (mergeWavBlobs), and get one real mp3 encode pass server-side at an explicit 96kbps (matched to the engine's native 24kHz mono output — higher would just be wasted file size, not more real quality) via a new/api/audio/encode-mp3endpoint.
[1.17.20] — 2026-07-19
Fixed
- Narrator's voice never reached synthesis even when properly assigned in the Library. "Synth all" in full-audiobook mode silently synthesized only the ~380 dialogue lines instead of all ~1325 narratable lines, with no error — the narrator's cast slot uses an emoji-prefixed sentinel key internally (
📖NARRATOR), andrehApplySharedCast()'s roster lookup lowercased that whole sentinel ("📖narrator") instead of translating it to the library's plain"narrator"key first, so the match always silently missed and the library's assigned voice never populatedrehState.narratorVoice— the exact fieldsynthAll()checks to decide whether to include narration at all.
[1.17.19] — 2026-07-19
Fixed
- Reverted 1.17.16 through 1.17.18's timeout tuning on the character-sheets fallback request — it was solving the wrong problem. Confirmed by comparing against a still-running, still-working session on a different book: the streaming endpoint never delivers a single visible delta for this generation (reproduced identically at 60s/300s/560s — not a matter of waiting longer), and the plain fallback request reliably completes in a couple of minutes when left alone. Adding a timeout to that fallback (as 1.17.16 did) meant every abort orphaned a thread holding the shared LLM lock (the underlying call can't be interrupted by a dropped connection), so the next attempt just queued up behind it and repeated the same doomed cycle — the timeout was actively causing the stall it was meant to prevent. The fallback is unbounded again, matching the working reference behavior; the stream's own idle-timeout is back to 60s since there's nothing to gain from waiting longer on an endpoint that never streams anyway.
[1.17.18] — 2026-07-19
Fixed
- 1.17.17's 300s timeout still aborted a genuinely-completing character-sheet passage, twice in a row — including immediately after a full restart with no pre-existing stuck threads. Confirmed live: this client-side abort doesn't actually stop the server from working — the backend's upstream call sits in an uninterruptible blocking socket read, so the abort just orphans a server-side thread that keeps holding the shared LLM lock until its own ~600s watchdog force-closes it, and the next retry then queues up behind that same abandoned thread. A short client timeout doesn't recover faster here, it actively makes things worse. Both timeouts raised to 560s — close to, but safely under, the server's own watchdog ceiling — so aborting stays a last resort instead of routine, and any abort that does happen leaves the orphaned thread already near its own expiry.
[1.17.17] — 2026-07-19
Fixed
- 1.17.16's timeout fix was too aggressive and made things worse. Confirmed live via the LLM server's own logs: a single character-sheet passage can run continuously for 4.5+ minutes with steady output the whole time — it was never stuck, it just doesn't emit a visible streamed delta until reasoning is done. The 60s/90s timeouts from 1.17.16 were aborting genuinely-succeeding generations mid-flight, discarding real progress and retrying from scratch — which looks identical to a real stall (repeated timeouts, zero progress) from the outside. Both timeouts raised to 300s, comfortably above the observed real duration and still safely under the server's own ~600s watchdog.
[1.17.16] — 2026-07-19
Fixed
- Character sheet generation could freeze indefinitely on one passage with zero feedback. Confirmed live: stuck on "Passage 2/114…" for 7+ minutes with no new requests, no error, no retry. Root cause: the streaming request has its own 60s idle-timeout, but the non-streaming fallback it falls back to on a stream failure had no timeout at all — when the backend's LLM lock was held by an abandoned stream thread stuck in a blocking socket read (a pre-existing server-side condition, only self-healing via a ~600s watchdog), the fallback request just hung for up to 10 minutes instead of failing fast into the existing retry-with-backoff logic. The fallback now aborts after 90s and retries like any other network error.
[1.17.15] — 2026-07-19
Fixed
- Studio's Cast Characters tab pointed at the wrong pass entirely. There are two unrelated things in the old app both loosely called "casting": dialogue speaker-attribution (Identify Characters tab, already wired) and passage-by-passage character PROFILE generation (appearance/backstory/voice notes, with live "Passage N / M…" progress and a per-character progress sidebar) — the old page's optional WF_STEPS step 3. An earlier fix (1.17.4) redirected this tab's empty-state button at the speaker-attribution menu instead, so it never actually offered profile generation at all, appearing to just go blank. Now calls the real entry point (
csForReader()) directly, rendering the same live-progress UI the old page has, inline.
[1.17.14] — 2026-07-19
Fixed
- Unknown-speaker dialogue whose preceding narration named the speaker with a plain-period inquit ("Garbaz rief von unten herauf.") stayed unresolved.
audiobookResolveUnknowns's existing rules only recognized a preceding inquit when it ended in a colon ("... sagte:") or when the speech tag came right after the quote — a Name + speech-verb sentence ending in a normal period, especially with extra words between the verb and the period ("...Ork legte den Kopf in den Nacken und schrie seinen Triumph zum Himmel."), matched neither. Added a third rule that checks the preceding narration's last sentence against the actual character roster (not a generic capitalized-word guess), so multi-word names/aliases resolve correctly too.
[1.17.13] — 2026-07-19
Fixed
- LLM casting could leave real dialogue lines completely unattributed right next to correctly-cast ones in the same passage. Confirmed live: "»Ich glaube, ich bin in dich verliebt.«" got correctly split and attributed to Alrik, while "»Halt, bleib stehen.«" and "»Ich liebe dich«" a few lines later stayed silently merged into narration with no speaker at all — same passage, same
»...«markers. The deterministic backfill that's supposed to catch dialogue the LLM misses only checked "did this chunk produce any dialogue at all" (.some()over the whole chunk), so it was satisfied by the first correct split and never re-examined the rest. Every leftover narration segment is now individually re-scanned and re-split. Already-cast passages need "Continue uncasted" or a targeted recast to pick this up — it doesn't retroactively repair segments already saved to a draft.
[1.17.12] — 2026-07-19
Fixed
- No visible way to import a document after deleting the current one. The paste/drag-and-drop import controls live inside the "Voice & synthesis settings" card, which is collapsed by default — fine once a document is loaded, but confirmed live as a dead end right after deleting the current book: an empty page with nothing indicating the import controls were hidden inside that collapsed card above it. The card now force-expands whenever no document is currently loaded (without touching a user's own collapse preference once one is).
[1.17.11] — 2026-07-19
Fixed
- Narrator scare-quotes (›Zelt‹) got split out as speakerless "dialogue" lines. German prose uses single guillemets ›...‹ for the narrator ironically/emphatically quoting a word within narration (e.g. "...die die Orks aufgeworfen hatten. ›Zelt‹ war eine sehr schmeichelhafte Bezeichnung..."), distinct from »...« for actual spoken dialogue — but the deterministic quote-splitter treated both the same way, producing standalone "Unknown"-speaker fragments for just the quoted word (confirmed live on book 2: "Zelt", "verlausten Pony", "Skipperedikt", "Seulaslintan", among others). Single guillemets are no longer treated as a dialogue delimiter anywhere in the casting pipeline. Already-cast books need their affected passages recast (e.g. "Continue uncasted" or a full recast) to pick this up — existing casts aren't retroactively repaired.
[1.17.10] — 2026-07-19
Fixed
- Relocated Cast Characters toolbar (1.17.9) reappeared as a stale duplicate at the bottom after any in-panel edit.
audiobookCastView()rebuilds the whole panel (a fresh#ab-cv-foot, same id) on every call, not just the first — confirmed live: after using "Split text to Unknown Speaker" to edit a line, a second, un-relocated footer (with "Open Script Rehearser" showing again) appeared back at the bottom while the relocated copy stayed at the top. Replaced the one-time move with aMutationObserverthat catches every rebuild, not just the first. - Leaving Studio for the old Read Aloud page would come back missing its own footer. The relocated toolbar was never part of the whole-panel borrow Studio already tracks for returning things to where they came from — it's now explicitly restored inside the panel before the panel itself goes home.
[1.17.9] — 2026-07-18
Fixed
- Character portraits fetched and embedded as raw base64 on every bulk character-list load, blocking rendering with no feedback.
/api/characters(used to build the Cast Characters grid, the Library grid, and elsewhere) returned every character's full base64 portrait inline; for a book with dozens of generated portraits this made the JSON payload and the resultinginnerHTMLwrite tens of megabytes, so Studio's Cast Characters tab could sit completely blank for a long time with no spinner — easy to mistake for broken. The list endpoint now returns a lightweight/api/characters/{id}/imageURL instead of the raw blob (single-character fetches for editing are unaffected). Guarded the database write path too: if a record round-tripped from the list ever comes back through a save with that placeholder URL still in itsimagefield, the existing stored portrait is now preserved instead of being overwritten. - Studio's Characters tab had a redundant, non-functional action row. The plain "Identify Characters"/"Cast Characters" tabs at the top just duplicated the labels of a richer dropdown toolbar (Identify Characters ⌄ / Cast Characters ⌄ / Cast ⌄) sitting at the bottom of the borrowed panel, and that toolbar's "Open Script Rehearser" button silently did nothing when clicked from inside Studio (its
navTo('s-rehearser')call was swallowed by the 1.17.8 nav guard, with no error shown). The dropdown toolbar is now relocated up to replace the plain top tabs, and "Open Script Rehearser" is dropped entirely — Studio already has its own Perform & Export tab for that.
[1.17.8] — 2026-07-18
Fixed
- Studio's "Cast Audiobook >" header button broke navigation. The old cross-section workflow stepper (
_wfUpdateHeaderNavinutils.js) injects its Previous/Next buttons into every section's header, including Studio's — but those buttons are hardcoded to the old Read Aloud/Library/Script Rehearsal sections and know nothing about Studio's own 4-phase system. Clicking "Next" from Studio's Source phase left the UI in a half-switched state: the step hint read "2/8 · Characters" while the tab bar still showed "Source" and the old "Already uploaded" card. The stepper's header nav is now suppressed entirely inside#s-caststudio— Studio's own Source/Characters/Voices/Perform & Export tabs are the only way to move between phases there.
[1.17.7] — 2026-07-16
Fixed
- Perform & Export rendered a blank Stage when reached without first visiting Characters in the same session.
_audiobook.segmentsis a purely in-memory cache, only populated by actually opening the Characters phase (which triggers the draft-restore); jumping straight to Perform & Export on a fresh page load left it empty even with a fully-cast draft already sitting on the server. Now fetches the server draft directly when needed instead of requiring that detour first.
[1.17.6] — 2026-07-16
Fixed
- Series voice-reuse (1.17.5) could hand a character a stranger's voice. Confirmed live: Lysandra's own character sheet lists "Kriegerin" ("the warrior woman") as a descriptive alias, and an unrelated placeholder character in book 1 happened to be literally named "Kriegerin" — the alias-matching direction treated that coincidence as "same person" and reused the wrong voice. Narrowed to exact-name matching only; a proper name repeating identically across books is a safe signal, a descriptive epithet coinciding with someone else's literal name is not (same class of false positive already found and fixed once this session for the identity-merge logic elsewhere). One character's voice on the live book was corrected by hand after the live repro.
[1.17.5] — 2026-07-16
Added
- Voice assignment now reuses the same voice across a multi-book series. The Character Library keys each character record per book (
book::name), so a recurring character (e.g. "Nyrilla" in episode 2 of a 3-part novel) had no automatic link back to her already-voiced record from episode 1 — auto-assign and voice design would pick/generate a fresh, differently-sounding voice every time instead of keeping the cast consistent. "Automatisch", "Design a Voice", and bulk "Auto-design voices" now all check (by name and alias, across every other book) whether this character already has a voice from a different episode first, and reuse it directly instead of assigning/designing a new one.
[1.17.4] — 2026-07-16
Added
- Library production sections are now collapsible, with a chevron toggle per book. Landing here scoped to one specific book (Studio's Voices phase, the old "Assign Voices" step) now collapses every other production and expands only the one being worked on, instead of dumping every book's whole roster on screen at once. Manual collapse/expand choices persist per book.
Fixed
- Studio's "Cast Characters" tab had noticeably less capability than the old page — it only offered a single "Cast from Reader" button (equivalent to "Cast all, from scratch"), missing "Continue uncasted characters", "Cast selected character roles", and "New recast" entirely. Root cause: in the old app these are two dropdown buttons ("Identify Characters ⌄" and "Cast Characters ⌄") sharing ONE footer on ONE panel — splitting them into two separate Studio tabs left the real menu only reachable from the "Identify Characters" tab, with this tab falling back to a much weaker built-in placeholder. Now points at and opens the real menu instead of duplicating a lesser version of it.
- Studio kept getting silently kicked back to the old Read Aloud section during long-running operations (opening a large book, running "Continue casting" on a partially-cast book) — several places in reader.js/audiobook.js call
navTo('s-reader')themselves as a "make sure the right section is showing" safety measure, including ones buried deep inside multi-second PDF parses or multi-minute LLM casting passes. Chasing and patching each one individually didn't scale (found three separate cases live). Replaced with a persistent guard: while Studio is the active section, anynavTocall targeting a section Studio borrows from (Read Aloud, Library, Script Rehearser) is dropped as an internal reflex — unless the user just clicked that section's own sidebar entry, which is unambiguous real intent to leave and is let through. Covers every current and future call site uniformly.
[1.17.2] — 2026-07-16
Fixed
- Studio's Source phase rendered unstyled/cramped (Speed/Seed/Temperature/Native speed as plain stacked rows, Paste/Drop-document stacked instead of side-by-side). Root cause: the CSS for this panel was scoped with
#s-readeras a required ancestor (#s-reader .reader-tuning-row, etc.) — accurate when the markup lived only in Read Aloud, but once Studio reparents the same DOM under#s-caststudio, those rules stopped matching entirely and it fell back to unstyled browser defaults. Extended the ~30 affected rules to also match under#s-caststudio.
Added
- "Already uploaded" book list in Studio's Source phase — the same Library "Books" list, so you can open a previously-imported book directly from Studio instead of only being able to paste/import fresh text.
[1.17.1] — 2026-07-16
Fixed
- Stray space before the closing German guillemet « in dialogue text extracted from PDFs (e.g. "hier. «" instead of "hier.«") — this scanned book's font renders the closing quote glyph as its own separate text item, and
readerBuildSentencesjoined every extracted word with an unconditional leading space, baking the stray space right in before it. The casting LLM then faithfully preserved it, since it's instructed to reproduce the source text exactly. Fixed to skip the leading space before any word that's purely closing punctuation («,",',',",),]). Applies to newly imported/extracted PDFs — a book already extracted before this fix keeps the existing spacing unless re-imported.
[1.17.0] — 2026-07-16
Added
- New "Studio" section (Speak menu) — a single 4-phase view (Source → Characters → Voices → Perform & Export) over the same PDF-to-audiobook pipeline previously spread across 8 confusing steps split between Read Aloud and Script Rehearser. It's a shell, not a rewrite: each phase borrows the exact same DOM/logic those sections already use (PDF import, LLM speaker-attribution casting, character sheets, the Library's voice-assignment cards, and Script Rehearser's Stage editor) via runtime DOM reparenting rather than duplicating any of it — so every bug fix made to those systems all session (voice-pill alias-merge redirect, Narrator card, emotion/speaker desync, etc.) applies here unchanged. Read Aloud and Script Rehearser stay in the sidebar, fully working, unchanged, while Studio is validated — nothing was removed.
- Perform & Export phase has one "Generate full audiobook" toggle replacing the old two-section split: on, Narrator lines get voiced and per-character "I play this" live-recording is hidden; off, Narrator stays silent (or reads via
Skip descriptions) and each character can be marked as user-played instead of TTS'd — the exact same underlying fields (skipDescriptions,narratorVoice,cast[x].voice==='me') Script Rehearser's own toolbar already exposed, just as one switch instead of two different screens.
Fixed
- Script Rehearser's Stage view could silently render as a completely empty page for books with a large, richly-illustrated cast. Root cause: the per-line character portrait (added earlier this session) embedded the character's full base64 image data directly into every dialogue line's HTML — a character speaking hundreds of lines re-embedded their own multi-hundred-KB portrait that many times, ballooning a real 1968-line script to hundreds of megabytes and silently failing to render. Fixed by serving portraits from a proper URL (
GET /api/characters/{id}/image, new route) that the browser fetches and caches once, exactly like voice pictures already do — instead of inlining the raw image data. This bug existed in the already-shipped Script Rehearser too, not just the new Studio section.
[1.15.14] — 2026-07-16
Changed
- Script Rehearser's Stage line rows now show the character's real portrait on the play button, instead of a generic "?"/voice-icon placeholder for any speaker whose assigned voice had no picture of its own — same portrait already shown in Cast Audiobook, Assign Voices, and the Cast sidebar strip right next to it. (Per-line emotion tag, edit-text pencil, and personal notes were already there — this closes the one visible gap.)
[1.15.13] — 2026-07-16
Fixed
- Speaker names didn't match the lines in Script Rehearser's Stage view, even though Cast Audiobook showed the correct attribution — confirmed live on a 1958-segment script:
parseScript's character-name-cue regex was ASCII-only ([A-Z...]), so any speaker name with a German umlaut or ß (e.g. "Turmwächter", "Freischärler", "Mädchen") uppercased to a string the regex couldn't match. Those cues were silently dropped — both the speaker line and its dialogue fell through to plain narration — which desynced the per-line emotions array (built with one entry per dialogue segment) from the actually-parsed dialog lines by one for every dropped cue. Six such names in this book meant every speaker/emotion pairing after the first drop point was shifted, eventually swapping entirely unrelated characters' lines. Fixed both the ALL-CAPS name-cue regex and the "CHAR: dialogue" colon-format regex to accept any Unicode uppercase letter plus ß.
[1.15.12] — 2026-07-16
Fixed
- "Next" button stuck disabled on Assign Voices (5/8), unable to reach Script Rehearser (6/8) at all. The Script Rehearser step was only enabled once
rehState.lines.lengthwas populated — but that's only ever set by loading a script into the Rehearser, which is exactly what clicking this step does. From a fresh session neither could happen first, permanently blocking the only path in. Now also enabled once the current Audiobook has cast segments ready, matching the fallback the click handler already builds from.
[1.15.11] — 2026-07-16
Fixed
- Narrator card wasn't clickable at all. The synthetic Narrator record added in 1.15.10 was inserted into each production's card list but never into the
byIdlookup map that_wireCharCardsuses to find each card's click target — so clicking it (voice pill, avatar, anything) silently did nothing.
[1.15.10] — 2026-07-16
Fixed
- Could not give an alias-duplicate card its own voice at all ("Kolon Tunneltreiber" — an alias-duplicate of "Kolon" — always redirected the write onto "Kolon" no matter what, blocking progress). The picker's "pick" and "Automatisch" actions now write directly to the exact record you clicked (by id) instead of running through the identity-scan that decides where writes "really" belong — that scan is right for automated casting passes avoiding duplicate creation, but wrong for an explicit, unambiguous per-card action. Use "Bibliothek an aktuelle Besetzung anpassen" afterwards to clean up duplicate rows once you've sorted out which one should stay.
Added
- Narrator can now get a voice from Assign Voices. The Narrator was never a real Library record (kept out of the cast library on purpose), so the only place to set its voice was the Script Rehearser's own Cast tab, in a rehearsal-local field that isn't shared elsewhere — easy to lose track of, especially now that the pipeline jumps straight to Stage. A Narrator card is now pinned first in every production's grid; picking a voice for it saves like any other character, and the Rehearser picks it up the same way it already does for the rest of the shared cast.
Changed
- "Hear a sample" play button in the banner is now green, matching the assigned-voice pill.
[1.15.9] — 2026-07-16
Fixed
- Picking a voice sometimes silently didn't save on the card you clicked. Root cause confirmed live:
clUpserttreats two library records as the same character whenever one'saliasesfield lists the other's name — "Kolon"'s aliases included "Kolon Tunneltreiber" and "Kolon der Zwerg" verbatim (leftover from an earlier casting pass that never got cleaned up), so assigning a voice to the still-separate "Kolon Tunneltreiber" card silently wrote it onto the "Kolon" record instead, leaving the clicked card looking untouched with no error. The picker now detects this redirect and shows a toast naming which record the voice actually landed on, with a pointer to "Bibliothek an aktuelle Besetzung anpassen" (Cast menu) to clear out the duplicate rows.
[1.15.8] — 2026-07-16
Changed
- "Hear a sample" play button moved back into the banner, directly in front of the voice pill, instead of at the bottom of the card — it had gone missing from easy reach after the voice info moved up to the pill in 1.15.6.
[1.15.7] — 2026-07-16
Fixed
- Voice pill overlapped the select checkbox in the top-left corner of Library cards — both were anchored at the same
top:10px; left:10px. Pill now starts after the checkbox's width instead.
[1.15.6] — 2026-07-16
Fixed
- Voice picker popup (Library detail page, card/table pick-voice buttons) had a transparent background, letting the page content underneath bleed through the list and make it hard to tell what was actually clickable. Root cause:
background:var(--card)referenced a CSS variable that was never defined anywhere (only--surface/--panelexist) — an invalid/missing custom property falls back to the property's initial value, which forbackgroundis transparent. Fixed tovar(--surface)(also fixed the same bug on.lib-tab).
Changed
- Library cards now show the assigned voice (or "Keine Stimme zugewiesen") as a clickable pill at the top of the banner, replacing the small icon-only badge — the voice is the single most important thing to check per character, so it's promoted to a glance instead of scrolling to the bottom of the card. Clicking it opens the same pick/search popup used elsewhere, now with a "Neue Stimme designen" quick action at the top for jumping straight to Voice Design. The bottom-of-card voice row now only shows the "Anhören" (hear a sample) button, since the name/empty-state text moved up to the pill.
[1.15.5] — 2026-07-16
Fixed
- Stage still showed 0/0 and an empty script/character pane after the 1.15.4 fix —
showPhase(3)only toggles which phase<div>is visible; the actual script text and character sidebar are built bybuildScriptPage(), which the direct jump-to-Stage call skipped. Now callsbuildScriptPage()(andhighlightCurrentLine()) before switching phase, same as the Stage sub-tab's own click handler already does.
[1.15.4] — 2026-07-16
Fixed
- "Script Rehearser" step in the pipeline stepper landed on an empty Stage (0/0 lines). Clicking the step number or the "Next" button from Assign Voices only navigated to the Rehearser section — nothing actually loaded the current cast/script into it, unlike the Rehearser's own "Rehearse" entry points which did. Now builds the Rehearser record from the live Audiobook segments (same cast and voices already assigned in Assign Voices) whenever no script is loaded yet, leaving an in-progress Stage session alone if one already exists.
- Skips straight to the Stage view instead of the Cast tab when opening the Rehearser from the Audiobook pipeline — by that point voices were already assigned in Assign Voices, so landing on Cast again was pure redundant re-work.
[1.15.3] — 2026-07-16
Changed
- Card name row now shows tier badge (Haupt/Neben) after the gender icon, same line, instead of before the name.
- Voice-assigned badge moved to the banner's top-right corner, left of the "change photo" camera button, instead of living inline in the name row where it landed at whatever height the vertically-centered name row happened to be.
Removed
- SillyTavern export button removed from Library cards (still available from the table view) — it was cluttering the compact card banner for a rarely-used action.
[1.15.2] — 2026-07-16
Changed
- Library card body facts (Occupation/Archetype/Gender/Age/Lines) are now a compact single-line list instead of a 2-column grid of boxes — each row only takes the height its own text needs, and a fact with no value is skipped entirely instead of rendering an empty box just to keep the grid aligned. Frees up meaningfully more room in an already-tight card.
- Library character cards now use a playing-card aspect ratio (5:7) for a uniform, deck-like grid. The body flexes to fill whatever space is left below the square banner instead of relying on a hardcoded height, so the card's total size is driven purely by its own width via
aspect-ratio. Reverts to natural (content-driven) height on mobile's single-column layout, where a 5:7 ratio at full viewport width would stretch the card absurdly tall.
[1.15.1] — 2026-07-16
Changed
- Library card banner is now a 1:1 square instead of a fixed 150px strip, so a saved portrait reads as an actual photo rather than a short crop. Its height now comes from the card's own width (already uniform across a grid row), and the card body below has a fixed height with internal scroll — together they keep every card the same total height without hardcoding one number that fought the new banner proportions.
- Removed the gender/age/lines pills from the banner overlay — they were duplicating the Gender/Lines stat boxes already in the card body. Age now gets its own stat box down there instead, next to Gender, rather than only ever showing on the banner.
[1.15.0] — 2026-07-16
Added
- "Clean up library to current roster" (Cast menu, Cast Audiobook view). Removes saved character records that aren't in the current casting run's roster — the Character Library accumulates a record for every name any past casting run has ever produced for a book with no cleanup tied to the current cast, confirmed live: 65+ saved records for a book whose current roster is 46 names, including spelling-variant duplicates ("Globo Brohm" / "Gombo Brohm" / "Gernot Brohm") and one character split three ways ("Kolon" / "Kolon der Zwerg" / "Kolon Tunneltreiber") that a past, less-accurate pass never merged away. Shows the full list of what would be removed before deleting anything.
Changed
- Library character cards are now a fixed size instead of growing with content. A character with lots of aliases/occupation text used to stretch its own card — and, since grid rows size to their tallest item, every other card sharing that row — noticeably taller than a sparse one right next to it, making the grid look ragged. Overflowing banner text now clips instead of growing the card; overflowing body content scrolls internally.
[1.14.99] — 2026-07-16
Fixed
- A character's real-name fields could get contaminated with a completely different character's name. Confirmed live: "Darrag" ended up with
first_name/last_nameboth set to "Riedmar" — a separate, unrelated cast character, not a revealed alias (Riedmar's own record was blank at the time, ruling out a merge-logic bug — the model itself confused whose name belonged where, likely from the two characters appearing together in the same passage). Now: if a generated sheet's first/last/full name exactly matches a DIFFERENT known character's name, that field is dropped instead of trusted. Manually corrected the live Darrag record. - "Cast from Reader" (and the equivalent Rehearser/selective-cast entry points) could silently land back on the plain Source view instead of starting the cast, showing an unrelated "resuming at sentence…" toast. Root cause not fully isolated (a race with the reader's own un-awaited async setup that runs every time its section becomes visible), but the practical fix doesn't need to be: the intended view is now re-asserted a moment later, winning the race regardless of which async step caused it.
[1.14.98] — 2026-07-16
Added
- Pencil icon to edit a character sheet's field values directly on the live casting card, next to the color-swatch button in the header. Each field saves on its own debounce timer, so editing two fields in quick succession can't cancel one another's pending save. Known limit: a field that's currently empty isn't rendered at all here, so adding a brand-new value to an empty field still needs the full Library profile page — this covers correcting a value that's already shown.
[1.14.97] — 2026-07-16
Added
- "Design Voice" and "Generate Profile Image" action buttons directly on the Voice Design / Character Image prompt boxes. Copy/Regenerate only ever acted on the prompt text itself — actually using it meant copying it out and pasting it somewhere else by hand. One click now does that directly with whatever prompt is already in the box.
Changed
- Removed the ST / TTS / Bild indicator columns from the Cast table. These only ever showed whether a prompt had been generated, which was redundant with information already visible elsewhere in the same row (the actual profile picture thumbnail, and the assigned voice in the Stimme column).
Fixed
- "Generieren" (voice) always built a fresh, cruder prompt from raw sheet fields, ignoring the properly-crafted Voice Design Prompt already generated and sitting right there on the card. Now uses the saved prompt when one exists, only falling back to the ad-hoc builder when nothing's been generated yet.
[1.14.96] — 2026-07-16
Fixed
- The voice picker popup ("Auswahl") could render floating several rows away from the button that opened it, in the Cast table view specifically. Root cause: the popup was appended as a direct child of the trigger element with
position:absolute— fine for a card-grid<div>, but the table view's trigger is a<tr>, and a<div>can't legally live inside a table row. Browsers silently relocate invalid table content out of the table structure, landing the popup at an unrelated position. Now appended to<body>as afixed-position popup anchored viagetBoundingClientRect, the same proven pattern already used for the alias and footer-menu popups elsewhere in this app — works identically in both the card and table views. - The live casting card's avatar only opened a bare file picker — no way to paste an image URL or regenerate one from the character's own image prompt without leaving the view. Now opens the same full lightbox (disk / URL / AI-regenerate) used everywhere else.
[1.14.95] — 2026-07-16
Added
- Library character cards now use an existing portrait as the whole card banner (full-bleed background with a bottom-fade for legible text), instead of squeezing it into a small 96px circle — a real photo deserves more than icon size. The avatar circle shrinks to a small "change photo" corner button in that case, so re-uploading still works the same way.
- A voice-assigned badge sits right next to each character's name in the card grid — filled green speaker icon when a voice is assigned, muted outline when not. Previously this was only readable as plain text at the very bottom of a (possibly long) card, so scanning a whole cast for "who still needs a voice" meant reading every card in full.
Fixed
- The live passage-by-passage casting view (Character sheets step) never showed an already-saved profile picture — neither the main card's avatar nor the "Characters found" sidebar list. Root cause: the seeding step that loads each character's seed sheet from their saved library record dropped the
imagefield entirely, so even a character with a real portrait always fell back to the plain letter placeholder in this view. Now carried through to both the card and the sidebar. - That same card's avatar was capped at a small square even with a real photo — now stretches to fill the full height of the colored header banner instead of sitting shrunk in the corner.
[1.14.94] — 2026-07-16
Added
- Play button on every cast character with a voice assigned, in both the Library's card and table views — hear a quick sample line in that voice without opening the full profile or leaving the page. Mirrors the Rehearser's own "Hear a line" button; toggles to a stop icon while playing. Since the Library only stores a character sheet (not actual script lines), the sample is a short generic greeting, in German or English depending on the character's own sheet text.
- "Continue uncasted characters" now skips characters with fewer than 10 lines by default instead of endlessly re-trying them. Confirmed live: a full-roster recast filled in the one prominent character and left every character under ~20 lines completely blank — a character with only a handful of lines usually just doesn't have enough text for the LLM to say anything real about them, so retrying burns a call that almost always comes back empty anyway. The toast now reports how many were skipped this way.
[1.14.93] — 2026-07-15
Fixed
- "Continue uncasted characters" could re-split an already-merged character back into two records forever. It checked whether a roster name already had a library record by exact name only, never checking aliases — so once a character-sheet pass proved two roster names (e.g. a pre-reveal alias and an already-established dialogue-attribution name) were the same person and merged them under one canonical name, the OTHER name still looked like "never cast at all" on every future run and got endlessly re-queued as its own separate target, recreating the split every time. Now also matches against every existing record's recorded aliases before deciding a roster name is uncasted.
- A second copy of the shared-debounce-timer bug, in the casting view's own inline character-detail panel (fixed once already this session in the Library's detail page, missed this second copy): editing two fields within 900ms could silently drop the first edit. Same fix — one timer per field instead of one shared timer.
[1.14.92] — 2026-07-15
Fixed
- Opening a character's full profile from the Read Aloud recast-results grid rendered it squeezed into a sliver with the sidebar overflowing to the side. That results grid (
.cs-list) is a CSS grid withminmax(300px, 1fr)card columns; the detail page's own two-column layout (main content + 240px character-navigation sidebar) got inserted as a single grid item, confining it to one column's width instead of the full row. The detail page now explicitly spans every column when it renders — a no-op in the Library's own plain container, so this only changes the one place that was actually broken.
[1.14.91] — 2026-07-15
Fixed
- Regression from earlier today: a character referred to mostly by an alias could end up completely blank after a full recast, even with 100+ lines of dialogue. The "output nothing if this passage is about someone else" instruction added to prevent cross-contamination (v1.14.89) was too broad — it also fired whenever a passage used a target's alias/nickname/role instead of their literal roster name (e.g. "der Schmied" instead of "Darrag"), even though the character's own already-recorded sheet said that alias belonged to them. Confirmed live: a synthetic "der Schmied" passage with Darrag's alias already on record returned an empty sheet before the fix, a fully-detailed one after. Caught this by directly comparing the API's raw output before/after against known real-book passages (Darrag/"Fremder" both had zero recorded sources despite 100+ lines each) rather than trusting the instruction wording alone. The model now explicitly checks the passage's own context AND the existing-sheets summary for an alias match before ever deciding a passage is unrelated.
[1.14.90] — 2026-07-14
Fixed
- "Verify all characters" could duplicate a paragraph into the cast, with a stray quote mark on the second copy. Confirmed live: the same narration paragraph appeared twice, separated by an unrelated dialogue exchange — the source PDF only had it once. Root cause: each recast group pads its request with ±5/+4 segments of surrounding context so the LLM has continuity across the group's actual targets, and adjacent groups' context windows can overlap by that much; splicing a group's multi-segment replacement back in can then reintroduce text a neighbouring group's overlapping window already restated correctly a few segments earlier. Added a dedup pass after the verification run that removes a near-duplicate segment found within a nearby window (normalizing away stray leading/trailing quote marks before comparing, since the duplicate copy often carries one) and reports how many it removed.
[1.14.89] — 2026-07-14
Fixed
- A field's source citation badge only ever showed the FIRST supporting quote, silently hiding any others. If "Relationships" was backed by 3 separate passages, only one citation mark ever appeared next to it — the rest existed in the saved data but had no way to reach them from that field. Now shows one small numbered mark per matching source, each numbered to match its position in the full "Sources / Evidence" list at the bottom of the card.
- Clicking a source citation only jumped to the top of the page, not the actual sentence. Now also runs the Reader's own search (highlighting + auto-scroll) for the quoted text right after the page jump, so the exact line is visible in context instead of "somewhere on this page, go find it."
- Character-sheet generation could cross-contaminate two different characters' profiles. When a per-character "evidence window" pass (used by "Continue uncasted characters" and manual re-cast) landed on a passage that was actually about a DIFFERENT character than the one being processed, the model had no valid way to say so — it wasn't allowed to invent a new profile, so it forced the unrelated character's information into the wrong profile's fields (caught live: a passage about "Marcian" got written into "Fremder"'s Notes field, along with the model's own visible confusion about what to do). The prompt now explicitly allows outputting nothing for a passage that isn't actually about any of the current targets.
[1.14.88] — 2026-07-14
Added
- 3rd casting quality pass: "Check voice consistency" (Identify Characters menu, Read Aloud → Cast Audiobook). Unlike the first two passes — which both re-read the source text chunk by chunk, since a whole book is far bigger than any context window — this one works purely over already-attributed lines already in memory: for each character with 4+ lines, it gathers every line credited to them from anywhere in the book (evenly sampled up to 60 for very talkative characters, so their arc late in the book is represented too, not just their first appearances) and sends that whole bundle to the LLM in one call, asking it to flag any line that doesn't match the voice established by the REST of that character's own lines. Catches a misattribution that reads fine in isolation but doesn't fit the character once you see their whole body of dialogue together — something no single passage-sized window could ever expose. Only ever reassigns to an already-known character name or 'Unknown', never invents a new one. New server route:
POST /api/audiobook-consistency-check. Caught live during testing: the model sometimes echoed its own sequential count instead of the real line index — added a quoted-excerpt field to the response and a text-match fallback so a flagged line can't silently misapply to the wrong segment.
[1.14.87] — 2026-07-14
Fixed
- "Failed to fetch" during character-sheet generation wasn't retried at all. The retry-with-backoff added earlier this session only matched 429/rate-limit errors — a plain dropped connection, DNS blip, or the backend restarting mid-run failed that passage permanently on the first hit, silently leaving it unprocessed for that run. Now covered by the same retry logic (shorter backoff than rate limits, since network blips usually clear faster).
[1.14.86] — 2026-07-14
Added
- "Cast selected character roles" now shows portraits and line counts, and is searchable/sortable. Replaced the bare alphabetical checkbox list with the same avatar + name + line-count row style as the "Characters found" sidebar in the live casting view, plus a search box and a Lines/A–Z sort — matching how you already pick characters everywhere else in the app.
- "Verify all characters" (2nd quality run) is now explicitly prioritized. It checks, in order: (1) every 'Unknown' line first — an unattributed line is worse than a misattributed one, (2) runs of 2+ consecutive segments assigned to the same speaker (narrator or character) — the most common place a hidden speaker-change or unquoted dialogue line gets swallowed into a narration/dialogue streak, (3) a general plausibility check on everything else. Previously all of this was checked in one unordered pass.
Fixed
- Stray quote marks were landing on the wrong segment instead of just being dropped. German uses » to open and « to close a quote (the reverse of French) — a dialogue turn's closing « sometimes ended up glued to the FRONT of the following narration segment instead of the end of the dialogue itself (and the mirror case for opening »), including cases where the entire "segment" was nothing but the stray mark. Added a mechanical post-cast fix that moves each mark back to where it belongs, plus a new explicit prompt rule telling the casting LLM the same thing so it happens less often in the first place.
[1.14.85] — 2026-07-14
Fixed
- "Verify all characters" (the second-pass quality check) was missing the post-quote inquit-tag rule. The first-pass casting prompt has an explicit rule for lines like
"Ich bin dagegen!" ... entgegnete Oberst von Blautann.(the dialogue comes before the name) — but the verification pass's rule list only carried over the colon rule ("X sagte:" → the following line is X's), not this one, so a Narrator-mistagged line immediately followed by an inquit tag wasn't reliably caught. Added the same "Nachgestellte Zuordnung" and action-beat rules the first pass already had.
[1.14.84] — 2026-07-14
Added
- Workflow navigation now says what to actually do. The Previous/Next buttons on every section header used to just say "Previous"/"Next" with no clue what either led to or when a step was "done" — they now show the target step's name, and a guidance bar under the header explains what this step is for and when it's safe to move on or go back to fix something.
- Adjacent same-speaker segments get merged after casting. Two or more consecutive segments assigned to the exact same speaker (narrator or a character) with no page break between them are either an attribution mistake or one continuous block chopped into noise by the per-passage LLM pass — both are now merged into one segment automatically, book-wide (not just within one passage), instead of leaving a wall of one-line cards to review by hand.
Fixed
- A character's voice reassignment could silently fail to save.
clUpsert's persisted-record builder computedvoice: prev.voice || sheet.voice, so once a character had ANY voice, every later reassignment (via the picker, "Auto assign", or "Design voice") updated the in-memory view and showed a success toast, but the database kept the OLD voice forever — the wrong voice then got used for every subsequent audio generation, with nothing anywhere indicating the change hadn't actually stuck. Precedence flipped so an explicit new value always wins. - Unrelated characters could get silently merged into one profile.
CL_IDENTITY_FIELDS(the persisted-library identity-match list) still includedtitle/other purely-descriptive fields, reintroducing a bug already fixed in the in-session merge logic (CS_IDENTITY_FIELDS) — two different characters sharing a job title or a reused epithet ("the Executioner") could have their sheets, backstory, and voice blended into a single record. Brought back in sync with the already-fixed list. - A failed TTS segment used to vanish from the exported/merged audiobook with zero warning. Both Read Aloud's page/sentence export and Script Rehearser's audiobook export caught synthesis errors per-segment and just moved on, so a mid-book backend blip produced a file "successfully" downloaded with paragraphs quietly missing — discoverable only by listening carefully. Both now count failures and refuse to export until they're fixed/retried, instead of silently shipping a gapped file.
- Generating character images could freeze the entire app for every user, for minutes. Every image-gen provider (ComfyUI, OpenAI, Google, OpenRouter) made blocking HTTP calls directly on the request handler with no
asyncio.to_threadwrapper — on this single-worker server, ComfyUI's own polling loop (up to 5 minutes per image) blocked the whole event loop, so a bulk "auto-generate images" run across a cast could make the entire site unresponsive for everyone, for as long as it ran, with no distinguishing symptom. Moved onto a worker thread like every other blocking call in this file already was. - Editing two character-profile fields in quick succession could silently drop the first edit. The detail page's autosave used one shared debounce timer for every field — switching from one field to another within 900ms cancelled the first field's still-pending save with nothing else to replace it, so that edit was never written to the record or persisted, with no error shown. Each field now gets its own timer.
- Rehearser's "Synthesize All"/"Re-synthesize stale" could get stuck locked forever. Same failure shape as the character-sheet generation-lock bug fixed earlier: an uncaught error partway through the loop (e.g. the current line's character got deleted mid-run) left the running flag stuck
true, silently no-op'ing every future click with no error shown. Both now run inside try/finally and skip a line whose cast entry has vanished instead of throwing.
[1.14.83] — 2026-07-14
Fixed
- Character-sheet recasts could silently stop saving forever, in one tab, with no error shown. The generation lock (
_cs.running) was only guaranteed to clear inside atry/finallythat started after the progress overlay was created — if that setup step (or anything else run once per call, before the passage loop) ever threw, the lock stayedtruepermanently. Every later recast in that same browser tab then hit the very first guard (if (_cs.running) return null) and did nothing at all: no toast, no save, no visible error — exactly indistinguishable from "recasting isn't working." Confirmed live: hundreds of/api/character-sheets/streamcalls succeeded over the last two days with zero correspondingPUT /api/characterssaves. The whole function body is now wrapped so the lock always releases and any crash surfaces as a toast instead of vanishing. A page reload already clears the flag in the meantime, since it's just in-memory state — no data was lost, but nothing new was saved until now.
[1.14.82] — 2026-07-12
Added
- API-key gate for external callers (Phase 4 of the roadmap) — off by default, since this is a live app and I couldn't test the same-origin detection against real browser traffic. Turn on "Require this key for external callers" in Settings → API Keys → External API Access once confirmed safe; the app's own UI is unaffected either way (same-origin, never needs the key). Also discovered the REST API was already comprehensive (118 documented routes, auto-generated docs live at
/docs) — this phase was mostly already done. - MCP tool set expanded (Phase 5) — from 4 tools (speak/transcribe/list_captures/list_profiles, discovered already existing at
/mcp) to 9: addedlist_books,list_characters,get_character,update_character,list_rehearsals. Note: full end-to-end "cast an entire book via one tool call" isn't included — that orchestration (chunking, streaming, progressive merge) currently only exists client-side in JS, not as a backend-callable operation; porting it is a larger follow-up, not silently skipped.
[1.14.81] — 2026-07-12
Added
- i18n expanded from German-only to 7 languages (Phase 3 of the roadmap: French, Spanish, Italian, Portuguese, Dutch, Polish, plus the existing German) — the translation infrastructure (
window.t,applyI18n, the language picker in Settings → General) already existed and worked, it just only had one language's dictionary filled in. Same key set (~100 UI-chrome strings: nav, section titles/subtitles, common buttons/labels) translated into all six new languages. LLM-generated content (character sheets, prompts) is unaffected — stays in the source book's language, as scoped.
[1.14.80] — 2026-07-12
Changed
- SillyTavern + Concept Art prompt boxes no longer silently disappear during an active recast — the live character-sheet card hid all four prompt boxes until their field had content, which made sense for Voice Design/Image (filled in live, passage by passage) but was confusing for SillyTavern/Concept Art, since those only ever run as a background pass after the whole recast finishes — they looked like they'd vanished rather than "not generated yet." Those two now always show, with a note explaining they generate after casting finishes while a run is active.
[1.14.79] — 2026-07-12
Fixed
- Character-sheet generation prompt hardcoded real character names from the user's actual book ("Zerwas", "Henker", "Vampir") as the identity-merging example, in both the client-side and server-side copies of the prompt. Beyond not generalizing to any other book, this risked the LLM getting confused by its own instructions calling out a name it was actively reading dialogue for — plausibly explaining why some major characters (Zerwas: 146+ lines) ended up with a completely empty profile despite having plenty of source material. Replaced with clearly generic placeholder names ('Marcus', 'the Executioner', 'Bloodfang') in both copies. No custom prompt is currently saved, so this takes effect on the very next run with no reset needed.
[1.14.78] — 2026-07-12
Added
- Focus trap + focus restore for every modal in the app (Phase 2 of the accessibility roadmap) — the ~11 places that build a
.audiobook-overlaydialog (confirmDialog, the avatar lightbox, csShow, voice pickers, etc.) never managed keyboard focus individually: Tab could escape a modal into the page behind it, and closing one never returned focus to whatever opened it. Fixed centrally via the existing DOM-mutation observer instead of touching all 11 call sites — Tab/Shift+Tab now cycles within the topmost open modal, and closing it restores focus to the trigger element.
[1.14.77] — 2026-07-12
Added
- "Continue uncasted characters" in the Cast Characters menu — a new option alongside "Cast all character roles" and "New recast (discard & rebuild all)" that only generates profiles for characters with zero detail yet (never cast, or only ever picked up as a bare name), skipping everyone already fully cast. Uses the same targeted evidence-window scan as "Cast selected character roles," just with the incomplete list built automatically instead of picked by hand.
[1.14.76] — 2026-07-12
Fixed
- Character sheet generation permanently dropped a passage's data on a rate-limit (429) error — a burst of requests hitting the LLM provider's per-minute cap (observed live: OpenAI 429 on passage 4 of 149) just logged the failure and moved on, silently leaving that section of the book with no character detail. Rate-limit errors now retry with backoff (up to 4 attempts, growing delay) before giving up; other error types still fail fast as before.
- Avatar lightbox and Rehearser cast card "More options" grid could overflow on narrow phones — the lightbox's action column had a 340px minimum width and the cast card's Language/Gender/Tags row stayed 3 columns regardless of viewport; both now collapse properly on mobile.
[1.14.75] — 2026-07-12
Fixed
- Script Rehearser Stage view was unusable on phone-width screens — the character sidebar was a fixed 220px column sitting next to the script page with no responsive handling at all, leaving almost no room for the actual script on a phone. It now stacks above the page and collapses full-width instead, same collapse toggle as desktop.
Investigated (Phase 1 of the mobile/accessibility/i18n/API roadmap)
- Spot-checked Library (Cast grid + table) and Read Aloud (Reader + Casting) for the same class of gap — both already handle narrow viewports reasonably well (auto-fill grids that naturally go single-column, flex-wrap toolbars, horizontally-scrollable tables) and didn't need immediate fixes. Broader page-by-page audit still pending for Phase 1 completion.
[1.14.74] — 2026-07-12
Added
- Script Rehearser Stage sidebar now has search, sort, and real character portraits — matching what the Cast tab already gained, instead of plain colored-letter dots. Names show in the character's cast color when a real portrait is available. Search/sort now only re-render the sidebar strip, not the whole (potentially 1000+ line) script page.
[1.14.73] — 2026-07-12
Fixed
- "Open Script Rehearser" could load stale/wrong cast data — it previously saved the current cast to IndexedDB and then re-fetched it by id before loading, which could hand back an outdated record (read-after-write race, or a stale id from an earlier pass). It now builds the record from the current in-memory segments and loads that directly — no re-fetch, no possible staleness — while still persisting to the DB in the background for the "Cast again" round trip.
- Casting could leave a lone orphaned »/« character as its own narrator segment — the casting prompt instructed the LLM to strip quote marks out of dialogue text entirely, and when it didn't fully comply, a stray quote mark landed as a standalone segment right before the actual dialogue. Quote marks are now kept attached to the dialogue text instead of being stripped (matching how they read in the source), which removes the failure mode rather than just re-emphasizing the instruction. Already-cast books need a recast to pick this up — existing segments aren't touched automatically.
[1.14.72] — 2026-07-12
Changed
- Script Rehearser cast cards now literally reuse the Library's own card (
_charCardHtml) for every speaker that matches a real character record — same colorful portrait/tier/occupation/alignment design as Library → Cast, not a look-alike. Clicking a card opens the same full profile page (voice/language/tags/soul editing lives there now); "back" returns to the cast list. Only "I play this", ignore/hide/delete, "Hear a line", and emotion chips remain on the card itself. Narrator and unmatched/"UNKNOWN" speakers (no Library record to reuse) keep the simpler fallback card with its own inline voice picker, since there's no profile page for them to point at.
[1.14.71] — 2026-07-12
Changed
- Script Rehearser cast cards decluttered — Language/Gender/Tags, the online voice-match panel, speaking-style prompt, and Character soul are now collapsed behind a single "More options" toggle instead of always showing five-plus form fields per card. Only portrait, name, voice, play button, and emotion chips show by default, closer to the Library's own card look. Kept as Rehearser's own card grid rather than embedding the Library table directly — this tab has to handle the Narrator and unmatched/UNKNOWN speakers, which the Library's book-scoped character table doesn't model at all.
[1.14.70] — 2026-07-12
Added
- Script Rehearser cast cards now show a real portrait, tier badge, and occupation/archetype — cross-referenced from the Character Library by matching the script's book/title, same visual language as the Library's own cast cards, instead of always falling back to a colored-initial avatar.
- Emotion chips on each cast card — every distinct speaking-tone tag used across that character's lines in the script (set via the existing per-line emotion picker) now shows as a small chip row, giving an at-a-glance sense of their emotional range before you even open the script.
[1.14.69] — 2026-07-11
Fixed
- Bulk image generation produced near-identical generic portraits for sparse background characters — a character with no physical/clothing/archetype detail (typical for 2-3-line crowd extras like "Ruf aus der Menge" or "Zwei Gestalten") fell back to a prompt with nothing character-specific in it ("an original character named X, ambiguous expression"), close enough across dozens of characters that several providers returned visibly identical results. Bulk generation now skips characters below a minimum-detail threshold instead of spending a generation on a near-blank prompt, leaving the neutral initial-letter placeholder instead.
[1.14.68] — 2026-07-11
Added
- Character profile pictures now sync to their assigned library voice's own picture — the Voices Library already had full picture upload/display support (
/api/voice/picture), it just never had anything feeding it from the character side, so voices only ever showed generic gender/type icons. Syncs whenever a character's image changes (upload, URL, AI-regenerate, bulk generate) if they already have a voice assigned, and whenever a voice gets (re)assigned (manually, auto-assign, or auto-design) if they already have an image. Best-effort — never blocks or shows its own errors, since it's a convenience mirror of the character's real picture, not the primary action.
[1.14.67] — 2026-07-11
Changed
- Avatar lightbox is much bigger — modal width 680px → 1180px, preview image 200px → 440px, larger text/inputs throughout. The first pass was sized like a small popup instead of something meant to actually judge image quality at a glance.
[1.14.66] — 2026-07-11
Added
- Avatar lightbox — click any character's profile picture in the Library grid/table to open a bigger preview with three ways to change it: upload from disk, paste an image URL (downloaded server-side to sidestep CORS, since most image hosts don't allow direct browser fetches), or regenerate with AI using an editable prompt (pre-filled with the character's current Image Prompt, with a per-run engine picker). Replaces the previous behavior where clicking an avatar jumped straight into a file picker with no way to preview, use a URL, or tweak the prompt before generating.
[1.14.65] — 2026-07-11
Fixed
- ComfyUI generation crashed with
NameError: name 'copy' is not defined— a leftover from moving the route's ad-hoc imports to the top of the file;copywas never actually added there. Caught by testing the endpoint directly before relying on it.
Configured
- Local ComfyUI wired to an actual working text-to-image workflow — a minimal 4-step Flux Schnell pipeline (checkpoint/CLIP/VAE already present on disk), verified end-to-end: submitted, polled to completion (~30s), and the resulting image confirmed as a real, on-quality character portrait. Chosen over the much larger "Consistent Character Creator" workflow specifically because it's simple enough to trust without ComfyUI's own UI export step.
[1.14.64] — 2026-07-11
Fixed
- Bulk "Auto-generate images" gave no visible progress until the entire batch finished — avatars only refreshed on the final grid re-render, so a long run (especially with a slow local ComfyUI workflow) looked frozen except for the toolbar's "N / total" counter. Each avatar now updates in place the moment its own image comes back, in both Cards and Table view.
[1.14.63] — 2026-07-11
Added
- Per-run image-generation engine picker in the Library toolbar — a dropdown next to "Auto-generate images" lets you pick OpenAI/Google/OpenRouter/Pollinations.ai/ComfyUI for just that run, overriding the Active Provider in Settings instead of having to go change it first. Defaults to whatever's currently active.
Changed
- Character avatar thumbnails in the table view are bigger (26px → 48px) — the previous size was too small to actually judge a generated image's quality at a glance.
[1.14.62] — 2026-07-11
Fixed
- Pollinations.ai now retries automatically on "queue full" errors — the free shared queue rejects requests under load instead of queueing them; a first live test failed this way and succeeded on manual retry, so that retry (up to 3 attempts, 4s apart) is now automatic.
Added
- Pollinations.ai card in Settings → Engines → Image Generation → Cloud APIs, alongside OpenAI/Google, documenting that it needs no key/setup — just select it as the Active Provider. Removed the stale "ComfyUI planned as a follow-up" note now that it's actually implemented.
[1.14.61] — 2026-07-11
Added
- Local ComfyUI as an image-generation provider (Settings → Engines → Image Generation) — paste an API-format workflow export (Workflow → Export (API Format) in ComfyUI's own UI, not a regular saved workflow), point it at your prompt node/field and output node, and character portraits/concept sheets generate on your own GPU instead of a paid API. Includes "Test connection" (reachability check) and "Test generate" (runs a real prompt through it and shows the result inline) so you can validate the setup without going through a full bulk-generate run. Deliberately does not attempt to auto-convert ComfyUI's editor-format graphs — workflows using virtual-routing addons (e.g. rgthree Get/Set nodes) don't show up as real graph edges and a naive converter could silently mis-wire them.
- Pollinations.ai as a free, no-API-key image-generation provider — a stopgap for when the paid providers' billing isn't sorted yet; third-party public service, no SLA or account needed.
[1.14.60] — 2026-07-11
Added
- Search box in the status bar's LLM model fly-up — OpenRouter alone lists 50+ models, making the plain scrollable list slow to use; typing now live-filters it. Applies to any fly-up with more than 8 entries (STT/TTS backend pickers included, though those rarely have that many).
- OpenRouter as a third image-generation provider (Settings → Engines → Image Generation), alongside OpenAI and Google — routes through the same chat/completions endpoint and API key as the OpenRouter LLM card (no separate key needed), for image-capable models like
google/gemini-2.5-flash-image-preview.
[1.14.59] — 2026-07-11
Added
- Connect and "Use as LLM" buttons on the Anthropic card too, for consistency with the other five Cloud API cards. Connect now probes correctly for Anthropic's actual auth scheme (
x-api-key+anthropic-versionheader) instead of the generic Bearer-token check the other providers use, which would have falsely shown "unreachable" even for a valid key. "Use as LLM" now warns before applying it, since this app's LLM calls are OpenAI-format (/chat/completions) and Anthropic's Messages API isn't compatible — the button works, but generation will likely fail until real Anthropic support is added.
[1.14.58] — 2026-07-11
Added
- Connect and "Use as LLM" buttons on the Cloud API cards (Groq, OpenRouter, Google Gemini, Mistral, OpenAI) — previously only the local Docker engine cards further up the page had these; the cloud cards only let you store a key with no way to test it or make it the active LLM. Connect probes the endpoint with the entered key and shows a persistent Connected state; "Use as LLM" applies the endpoint, key, and a sensible default model to Settings in one click, matching the local cards' behavior. Not added to the Anthropic card since it isn't OpenAI-compatible and would silently break.
[1.14.57] — 2026-07-11
Added
- OpenAI and Anthropic cards in Settings → Engines → Language Models → Cloud APIs, alongside Groq/OpenRouter/Google Gemini/Mistral — same API key storage pattern as the existing cards. Anthropic is flagged as not OpenAI-compatible (needs the Messages API, not
/chat/completions), since every other card here — including this app's own LLM calls — assumes an OpenAI-compatible/chat/completionsendpoint.
[1.14.56] — 2026-07-11
Changed
- The image-generation Model field is now a dropdown of known models per provider, not a blank text box — there was nothing to "select" because it was free text with only a greyed-out placeholder, so you had to already know a valid model ID to use it at all. Now shows curated options for OpenAI (gpt-image-1, dall-e-3, dall-e-2) and Google (gemini-2.5-flash-image and its preview variant, imagen-4.0 and imagen-3.0), plus a "Custom…" option that reveals the free-text field for anything newer than this list. Switching provider resets the model to that provider's default instead of silently keeping an incompatible model ID from the other one.
[1.14.55] — 2026-07-11
Fixed
- Clicking a character card in the fresh-recast results grid (Read Aloud → Character sheets) did nothing — the detail page always rendered into
#lib-chars-list, a container that only exists on the Library page. It now accepts an explicit target container and back-navigation callback, so opening a profile from any card grid in the app (not just the Library) works and "back" returns to wherever you actually came from.
Added
- SillyTavern and Concept Art prompts are now generated automatically as part of a recast — previously only the Voice Design and Image prompts were filled in during the passage-by-passage pass; the other two needed a manual "Generate" click per character. They now run as a background pass right after a recast finishes (its own progress/result toast), using the character's complete profile in one LLM call per prompt.
- "Clone" as a fourth voice option on the character profile page, alongside Pick/Auto/Online/Design — opens Clone a Voice with the character's name pre-filled, ready for a mic take, file, or YouTube URL (cloning needs a real audio source only the user can supply, so this just gets them to the right screen instead of automating a step with no sane default).
- Explicit Upload / Search online / Generate buttons under the profile picture, instead of only an upload-on-click avatar plus one AI-generate button — "Search online" opens an image search in a new tab (built from the character's name, book, and archetype) for the user to browse and save an image themselves.
[1.14.54] — 2026-07-11
Fixed
- The "Delete characters?" confirmation dialog's red confirm button was invisible —
.btn-dangerand a few other rules (error toasts, danger menu items) styled themselves withvar(--error), but no--errorCSS variable was ever defined (only--red), so the button rendered with no fill at all instead of solid red — it looked unclickable rather than like a button.--erroris now defined alongside--redin both the light and dark themes.
[1.14.53] — 2026-07-11
Fixed
- Bulk actions in the Character Library only reported a fail count, never the actual error — "Generating images finished for 0 characters (79 failed)" gave no way to tell why without opening DevTools. The toast now includes the actual error message (e.g. an API quota/billing limit), and a bulk run stops early after 3 characters in a row fail with the identical error instead of grinding through the rest of a selection hitting the same systemic wall.
[1.14.52] — 2026-07-11
Changed
- Bulk voice-design sample text now falls back to a real quote from the book before the generic filler sentence — the fallback only kicked in when the character had no attributed dialogue line in the currently-loaded audiobook cast, which is the common case when designing in bulk straight from the Library (outside that book's cast context). It now tries the character's own sourced quotes next, preferring one that reads like something they actually said, before resorting to the neutral placeholder line.
[1.14.51] — 2026-07-11
Fixed
- Bulk-designed character voices showed up as "Clone" instead of "Design" in the voice library — the headless auto-design flow saved a reference sample the same way manual cloning does, but never tagged the result with the
origin: designedmarker the UI's Clone/Design classification checks for. All 75 already-saved auto-designed voices were retagged in place.
Added
- Bulk-designed voices are now grouped and tagged by the book they were designed for — same
group/tagmetadata the Script Rehearser's own voice-design flow already sets, so a book's character voices cluster together instead of scattering through the flat voice list.
[1.14.50] — 2026-07-11
Added
- Select-all checkbox in the Character Library table header — checking it selects/deselects every row directly from the column it lives in, in addition to the existing toolbar "Select all" button; the two stay in sync (including an indeterminate state when only some rows are checked).
[1.14.49] — 2026-07-11
Fixed
- Every page went blank right after the 1.14.48 deploy — the new header Previous/Next injection (
_wfUpdateHeaderNav) was called with acurIdxvariable that only existed inside the stepper-rendering loop, throwing aReferenceErroron every singlerefreshWorkflowCrumbs()call (which runs on essentially every page render). The exception aborted whatever page-render code ran after it in the same call stack, leaving pages showing only their header.curIdxis now computed once, before the loop, in scope for both the stepper and the header-nav update.
[1.14.48] — 2026-07-11
Added
- "Select all" and "Delete selected" in the Character Library grid — select-all toggles every checkbox in a production at once; delete removes the checked records outright (there was previously no delete option at all in this grid — needed for clearing stale/corrupted entries before a fresh recast).
- Previous/Next workflow buttons on every page's section header, not just the stepper widget further down — same navigation, one click away without scrolling.
Changed
- The stepper's back-to-previous-step button moved from the title row down to the numbered-steps row, sitting with the rest of the step navigation instead of next to the book title.
[1.14.47] — 2026-07-10
Added
- "Auto-design voices (selected)" and "Auto-generate images (selected)" bulk actions in the Character Library grid — auto-design is fully headless (generates and saves a brand-new voice per checked character from their Voice Design Prompt, no navigation to the Design screen needed); auto-generate calls the configured image provider per character and saves the result as their profile picture. Both run sequentially over the selection with live progress.
[1.14.46] — 2026-07-10
Fixed
- Character gender was silently wiped to blank for every non-English answer — the character-sheet cleanup validated
genderagainst only the literal strings "male"/"female"/"nonbinary"; a German book naturally gets German answers ("weiblich"/"männlich"), which failed that check and got discarded instead of normalized. Now maps common English and German variants to the canonical value.
[1.14.45] — 2026-07-10
Fixed
- The page could hang/appear blank for many seconds on load — a
MutationObserverre-ran a full-document accessibility scan (severaldocument.querySelectorAllpasses over the entire page) on every singleclassattribute change anywhere in the app, instead of just the changed element. This session's larger dynamic grids/cards made class mutations frequent enough to compound into multi-second main-thread blocks (visible asrequestAnimationFrame/setTimeoutviolations). The attribute-change handler now only does the cheap, targeted nav-active-state sync it actually needs, not a full re-scan.
[1.14.44] — 2026-07-10
Changed
- Character Image Prompt now asks for a full reference/turnaround sheet instead of a single portrait — full-body front view, side/back turnaround, a small expression sheet, a color palette swatch, and labeled prop/clothing callouts in one composite image, matching production concept-art conventions. Applies to both the LLM-generated prompt and the client-side fallback used before that's been generated.
[1.14.43] — 2026-07-10
Added
- New "Assign Voices" workflow step between Cast and Script Rehearser — jumps straight to the current book's block in the Character Library grid (voice pick/auto/online/generate, AI image generation, and per-character sheet regeneration already live there) instead of landing on the whole cross-book library unfocused.
- AI-generated character profile pictures — a new "Image Generation" page under Settings > Engines lets you configure OpenAI (gpt-image-1) or Google (Gemini 2.5 Flash Image) with an API key; a "Generate" button next to each character's avatar in the full editor builds the image from the character's Image Prompt and saves it as their profile picture. Local ComfyUI (no API key needed) is a planned follow-up.
Fixed
- Fixed a real "NaNNaNNaN" rendering bug in the character card grid — a stray double
+operator (+ + stat(...)) coerced HTML strings to numbers instead of concatenating them. - Character card avatars in the table view were rendering at full card-grid size (~90px) and overlapping the name text — a CSS specificity bug where the base
.lib-char-avatarrule (declared later in the file) silently beat the smaller.lib-chars-tbl-avataroverride regardless of source order. Fixed with a properly-scoped selector; checkbox and avatar columns are also narrower now. - The "Cast Characters" results grid and other browsing grids were rendering full detail cards instead of compact summary cards — unreadable wall of expanded profiles. All cast-card grids (Library, post-generation results) now render the same compact card (name, occupation, archetype, main/side, lines, gender, voice, good/evil rating) and open the full profile on click.
Changed
- The character table view is now sortable by clicking column headers (Name, Gender, Age, Lines, Language, Alignment, Voice) — reuses the same comparators as the existing Sort dropdown, with a direction toggle and arrow indicator.
[1.14.41] — 2026-07-10
Changed
- Character Sheets fields render as a compact list (label left, value right, thin separator) instead of stacked label-above-value blocks — closer to the old flat field list while keeping the grouped 2-column sections.
[1.14.40] — 2026-07-10
Changed
- Destructive confirmations (Identify all characters, New recast) now use an in-app dialog instead of the browser's native
confirm()popup — the native one exposed the raw server IP/URL ("192.168.178.8:7890 says…") and couldn't be styled to match the rest of the UI. - A character with no generated fields yet shows a plain "profile hasn't been generated yet" note instead of a blank gap — it was indistinguishable from data having disappeared.
[1.14.39] — 2026-07-10
Changed
- The Character Sheets sidebar now defaults to sorting by most lines first instead of by generation progress — main characters (who naturally have the most dialogue) surface at the top right away instead of waiting to be alphabetically or recently touched.
[1.14.38] — 2026-07-10
Fixed
- "Waiting for streamed JSON output…" could get stuck forever even though generation kept completing passages normally in the background — the fallback message only fired when the streaming request itself failed and fell back to blocking; if streaming succeeded but the model simply never emitted incremental deltas (some models buffer the whole answer into one final frame), nothing ever replaced the placeholder text, making a healthy run look permanently hung.
[1.14.37] — 2026-07-10
Fixed
- The Passage/Live Output resize handle was inverted — dragging up shrank the panel and dragging down grew it, backwards from what a top-edge handle should do (regression from moving the handle from the bottom-right corner to the top edge).
- "Lines" on the character header/sidebar was always blank during live generation — line counts were only computed after the entire run finished; they're now tallied from the already-cast dialogue up front and attached to every sheet from the start, so they show immediately instead of staying blank until completion.
Changed
-
The Character Sheets sidebar is now a two-row card per character — a bigger avatar spans both rows, the name on top, "N lines | filled/32" together underneath, instead of cramming avatar/name/count into one line.
-
Character sheet generation could silently merge completely unrelated characters into one sheet — the merge logic treated a shared title/occupation/age/race/language/social-class string as proof two characters were the same person, so different characters that happened to share a generic title (e.g. several villains here all carry "Verweser der von den Orks eroberten Reichsprovinzen") got smashed into a single corrupted sheet, with each wrongful merge dumping the other character's name into "Also known as". Matching is now based only on genuine name/alias fields. Sheets already corrupted by this need a regenerate (Cast Characters → New recast) to clear up — the fix only prevents it going forward.
-
Character Sheets live preview now shows the exact source sentence behind each field — click the small numbered mark next to a field to see the quote it was drawn from, instead of only a hover tooltip, making it easy to spot a misread at a glance.
Changed
- Character Sheets live preview and card layout overhauled to match the richer casting Profil view — 2-column grouped sections (Identität / Erscheinung / Persönlichkeit / Geschichte / Fähigkeiten / Konflikt / Beziehungen) instead of a flat field list, a full voice-picker row (Auswählen / Automatisch / Online suchen / Generieren), and the four prompt boxes (Voice Design, Image, SillyTavern, Concept Art) now only appear once actually generated instead of always showing "not generated yet" placeholders.
- Removed the duplicated Occupation/Archetype/Also-known-as lines in the character header — they used to appear twice (once as plain text, once as pills further down); now shown once, as pills, in the position the plain text used to occupy. Added a "Lines" count and a highlight-color picker to the header too.
- The Character Sheets sidebar can now be sorted — Progress (default), A–Z, Lines, or Gender, matching the sort control already on the casting roster sidebar.
Fixed
- Character Sheets no longer pulls in stale characters from earlier casting attempts — the sheet generation list was unioning the current live cast with every character record ever saved for the book, including junk entries ("Unbekannter Mann", "Turmwächter", etc.) from before recasts/merges/quality runs cleaned things up. It now only reuses saved records that still match a name in the current cast.
- Character line counts on the Character Sheets panel were silently always zero — the code treated the roster (a plain array of names) as a lookup map, so counts never attached; now tallied directly from the segments.
Changed
- The Reader's step wizard is now two rows instead of one horizontally-scrolling strip — book title (with back-to-previous-step arrow) on top, the numbered step sequence below, so the steps aren't hidden behind a scrollbar.
- The Casting audiobook footer is now three grouped flyout buttons instead of eight flat ones — "Identify Characters" (all / unknown / verify), "Cast Characters" (all / selected / new recast — a fresh rebuild that discards existing sheets), and "Cast" (view / export). "Open Script Rehearser" and "Continue casting" stay as direct one-click buttons. The flyouts are fixed-position and anchored to their trigger button (not nested in a clipping container), unlike the old dropdown this replaces a second time.
- The Character Sheets "Passage / Live output" panel now has a proper drag handle on its top edge — a small centered grip bar you drag up/down to resize, replacing the boxed icon button that floated awkwardly over the bottom-right corner of the output text.
[1.14.32] — 2026-07-10
Changed
- "Verify all characters" is now a real second opinion, not a rerun of the first pass — it uses a distinct plausibility-check prompt that judges whether each existing label actually holds up (rather than reclassifying from scratch), now also re-examines every Narrator line specifically to catch spoken dialogue that got swallowed into narration, and resolves any name the model returns against saved aliases in code (e.g. "Vampire" → "Zerwas") instead of leaving alternate names as separate/Unknown speakers.
[1.14.31] — 2026-07-10
Fixed
- Merging a character now only redraws the lines that actually changed speaker, instead of the entire feed — a 2-line alias merge on a large book was rebuilding all ~2000 rows (each re-running the highlight regex pass), which is what made even a tiny merge take as long, and as freeze-prone, as opening a freshly cast book. Affected rows are patched in place now; only merges touching hundreds of lines still show the progress bar for more than an instant.
[1.14.30] — 2026-07-10
Changed
- The characters sidebar now shows skeleton placeholder rows while a live cast is still reading and hasn't found any characters yet, instead of a static "reading…" label, so it's obvious the panel is actively working rather than stuck.
[1.14.29] — 2026-07-10
Fixed
- Restored the "Add alias / also known as" tag button on each character in the casting roster — it was silently dropped from the roster row template during the recent Library cast card rework, even though the popup and merge logic behind it were still intact.
[1.14.28] — 2026-07-10
Fixed
- Rebuilt the stale
static/dist/main.min.jsbundle —loader.jsalways tries this prebuilt bundle before falling back to individualstatic/js/*.jsfiles, so any edit to those files (however many restarts or cache-busts) had no visible effect until the bundle itself was regenerated withnpm run minify.
[1.14.27] — 2026-07-10
Fixed
- Reopening a large already-cast audiobook no longer freezes the tab — restoring a saved cast with thousands of segments now redraws the feed in animation-frame batches instead of one giant synchronous pass, so the browser stays responsive instead of showing "Page Unresponsive" and looking like casting silently restarted on its own.
Changed
- Character views now share a cleaner header and a wider overview card layout — the Library cast cards are larger and more graphic, the profile header keeps the important identity info without duplicating aliases, and the audiobook profile view now shows line count with separate Auto refine and Edit actions.
- The audiobook footer buttons now read like the actions they perform — Identify unknown characters, Identify all characters, Verify all characters, Cast all/selected character roles, View cast, export, and Script Rehearser are all direct buttons in a clearer left-to-right order, so the old flyout-style wording is gone.
- The Library cast table now uses a much smaller profile icon and tighter sticky columns — the avatar no longer dominates the first columns, so the name and metadata scan more like a real table.
1.14.24 — 2026-07-09
Fixed
- Character sheet sources are visible again and appear before the prompt boxes — the evidence section now renders any source metadata we have, instead of hiding everything that lacks a long quote.
1.14.23 — 2026-07-09
Changed
- The character-sheet header is less cluttered — aliases now live in the identity block instead of being duplicated as header chips.
1.14.22 — 2026-07-09
Fixed
- The Character Sheets live layout no longer nests the side panel under the left column — the output box and character roster are back in separate columns, and the output box keeps its visible resize handle when collapsed.
1.14.21 — 2026-07-09
Added
- A one-character refine action in the audiobook profile panel so mixed profiles can be re-run on a single selected character instead of recasting the whole book.
Changed
- Character Sheets and audiobook profiles now expose the same identity fields — first name, last name, full name, aliases, gender, title, and occupation/profession are shown explicitly so one unified sheet layout is easier to read and edit.
- The live Passage / Live output box now has a visible drag handle even when collapsed so the user can tell it is resizable and adjust how much of the streaming output stays visible.
1.14.20 — 2026-07-09
Changed
- Character Sheets now puts prompt generation at the bottom of each sheet with collapsible ready-to-copy boxes for Voice Design, Character Image, SillyTavern Character, and Character Concept Art prompts.
- Character sheet evidence is easier to audit — source hooks now show the supporting quote snippets, and alias labels are written as “also known as” in the UI.
Fixed
- The cast-character flow no longer invents new profiles while refining an already cast roster — selective recasts now focus on the existing cast and use nearby paragraph windows around each hit as context.
1.14.19 — 2026-07-09
Changed
- Cast Characters is now a dedicated page instead of a popup — the audiobook cast flow now has a new
Cast Charactersstage betweenCast AudiobookandCast, with the oldCast Charactersstage renamed toCast. The new page keeps the cast-character content and actions, but uses the same large-page layout asCast Audiobookso the UI is easier to work with.
Fixed
- Conversation Playground thinking text was no longer visible while streaming — the reasoning panel now auto-opens as soon as the first
thinkingchunk arrives, instead of staying collapsed until clicked.
[1.14.18] — 2026-07-09
Fixed
- Every retried/recast audiobook cast created a new duplicate Rehearsal Library entry instead of overwriting the previous attempt — a fresh (non-resume) cast unconditionally nulled the in-memory link to the existing SQLite record, even when retrying or recasting a book that already had one from a prior attempt. The link is now only cleared for a genuinely first-ever cast; retries and "Recast all" overwrite the existing record instead of piling up near-duplicates.
[1.14.17] — 2026-07-09
Added
- Editable "Prompt" panel for Character sheets, matching the one already on Casting audiobook — a button in the progress modal's header opens a textarea with the full extraction prompt (all ~25 fields + the sources/citation schema), with named-preset save/load/delete. Edits persist as the new default and take effect from the next passage onward (even mid-run), via a new
character_sheets_promptoverride accepted by_charsheets_prepare()server-side (shared by both the blocking and streaming endpoints, so it can't drift between them).
[1.14.16] — 2026-07-09
Changed
- Redesigned the Character sheets progress modal — was a small (420px) dialog with a collapsible single-line JSON preview and a cramped chip grid. Now a large panel (like the audiobook casting live view): the passage currently being analysed on the left, the LLM's live JSON output filling in on the right, and a character sidebar on the right edge with the character(s) just updated highlighted — so you can watch each sheet actually being built from its source text.
[1.14.15] — 2026-07-09
Added
- Two new attribution rules for the casting prompt: a mid-quote " - " (em-dash pause) no longer causes the model to treat it as the end of a quotation and hand the rest to a wrong/new speaker; and narration that names whose voice is about to speak (e.g. "Marcians Stimme wirkte...") now resolves the following unattributed line to that character instead of leaving it 'Unknown'. Applied to the default prompt and auto-migrated into already-saved custom prompts (same mechanism as the earlier Doppelpunkt-Regel upgrade).
[1.14.14] — 2026-07-09
Fixed
- Clicking a name elsewhere in the text to feed an already-open "Assign to" popup did nothing — a separate document-level "click outside closes the popup" handler only exempted clicks on the paragraph's own speaker label, not clicks anywhere else in the passage text. So the popup was being closed by mousedown before the (v1.14.13) name-click logic ever got to run. Clicks and drag-selections inside passage text no longer auto-close the popup.
[1.14.13] — 2026-07-09
Fixed
- Clicking or drag-selecting a name could reassign the wrong paragraph — the intended flow is: click the broken (Unknown/Narrator/misattributed) paragraph's speaker label first to open "Assign to", then supply the name by typing, clicking it anywhere in the text, or drag-selecting it. But clicking/dragging a name with no popup open yet used to open (and target) the popup for whichever paragraph that name happened to live in — silently reassigning that paragraph instead of the one the user meant to fix. Clicking/dragging a name now only ever fills an already-open popup; a paragraph is only ever selected via its own speaker label (or the existing double-click-a-name fast-assign shortcut).
[1.14.12] — 2026-07-09
Added
- Character avatar images in the casting pickers — the Characters Found sidebar, the "Assign to" popup's character list, and the "merge with an already-found character" list now show the character's saved portrait (same source as the profile detail panel) when one exists, falling back to the colour-keyed initial-letter dot otherwise.
[1.14.11] — 2026-07-09
Fixed
- Clicking a name inside a paragraph's text silently retargeted the assign popup to that paragraph — misleading when you open "Assign to" on one (e.g. Unknown) paragraph, then click a name mentioned in a different paragraph just to reuse it: the popup would jump to and highlight that other paragraph instead of staying on the one you meant to fix. Now, while a popup is already open, clicking a name elsewhere only fills the search box — it no longer changes which paragraph gets assigned. Clicking a paragraph's own speaker label still always retargets it, as does clicking a name inside a paragraph when no popup is open yet.
[1.14.10] — 2026-07-09
Added
- Passage/Thinking panels in audiobook casting's live view are now resizable — drag the bottom-right corner of either box to read more comfortably.
Fixed
- Character-list collapse chevron pointed the wrong way — it showed the direction opposite to where the panel edge actually moves. Now expanded shows → (collapsing moves the edge right) and collapsed shows ← (expanding moves it left).
[1.14.9] — 2026-07-09
Fixed
- "Thinking" panels stayed empty for models served by some vLLM builds (confirmed on
vllm-0.23.1rc1) — those backends stream reasoning under a plainreasoningkey instead of the more commonreasoning_contentkey. Every place that reads reasoning text (Conversation Playground, audiobook casting's live view, character sheets) now checks both, via a shared_reasoning_text()helper. Root-caused by directly probing the model's raw streaming response and finding"delta":{"reasoning":"..."}instead of the expected field name.
[1.14.8] — 2026-07-09
Changed
- Audiobook casting timeouts raised for large/slow models — first attempt per passage: 6 min → 10 min; half-passage retry: 3 min → 5 min. 10 min also matches the server's own timeout cap, so this is the practical maximum without also raising that cap.
[1.14.7] — 2026-07-08
Fixed
- Audiobook casting's "LLM Reading…" live view showed the passage three times — once in the reader pane above it, once in a "Passage" column, and once more in a full-width block toggled by the chevron (which duplicated the "Passage" column exactly). Removed the redundant full-width block; the chevron now shows/hides the two-column view itself (previously it appeared automatically once the LLM's first token arrived and had no way to be hidden again). Swapped the column order to Passage (left) / Thinking (right) to match the Conversation Playground's layout.
[1.14.6] — 2026-07-08
Added
- Collapsible "Thinking" panel in the Conversation Playground — reasoning models' chain-of-thought is now separated from the actual reply: it streams into a collapsed panel behind a "Thinking" chevron (click to expand/collapse) instead of appearing inline in the chat or being spoken aloud. Handles both API styles: a dedicated
reasoning_contentdelta field, and inline<think>...</think>blocks — including chat templates that inject the opening<think>as a fixed prompt prefix, so it never appears in the model's own stream (only the closing tag does).
Changed
- Noise-gate slider moved next to the mic controls in the Conversation Playground — it was sitting on its own row to the left of the text input; now it's grouped with Auto-stop/Hands-free/Live agent since it's a mic-input setting.
[1.14.5] — 2026-07-08
Fixed
- "Add Custom" engine dialog was invisible/unclickable from the Speech Recognition and Language Models tabs — two unclosed
<div class="card">tags in the TTS settings markup left the dialog (and everything after it) nested inside the TTS panel's DOM subtree. Since that panel isdisplay:nonewhenever another Engines tab is active, the dialog opened with zero size and no way to interact with it. Closed the missing tags so the dialog renders as a top-level element. - Uncaught
ReferenceError: Cannot access '_localContainersLoadSeq' before initializationthrown on every page load when the Engines tab was active — aletdeclaration for the container-load sequence counter was placed after the code that first used it. Moved the declaration above its first use.
[1.14.4] — 2026-07-08
Fixed
- "Apply effects" button stayed disabled after generating a preview — streaming playback (the default, lower-latency mode) never produces an in-memory audio blob, and the button's enable logic required one. It now enables regardless of playback mode; if effects are applied before a buffered blob exists, one is synthesized on demand from the same voice/text/instruct/backend used for the preview.
[1.14.3] — 2026-07-06
Added
- Rich voice-picker dropdown for App Routing's output voice field — same searchable, avatar-thumbnail dropdown used elsewhere in the app, attached as a browse button next to the field. The field itself stays free-text so it can still target
vd_Voice Design presets that aren't part of the voice library. - Gender, Tag, Rating, and Group filters restored/added to the My Voices table — the Gender and Rating filtering logic already existed in the code but its
<select>elements had been left out of the visible layout after an earlier redesign (silently no-op'ing the whole filter-population function, which also broke the already-visible Language/Type dropdowns). Added the missing controls plus new Tag and Group dropdowns wired to the same filter state the sidebar's group/tag chips already use.
Fixed
- Audio effects (reverb/compressor/chorus/pitch) failed with "pedalboard is not installed" even though it was —
pedalboard's native extension links againstlibatomic.so.1, an OS-level shared library missing from the container image. Addedlibatomic1to 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/renameendpoint); 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 astream=Truecall 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-levelyieldpoint), 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: stickywith atop: 40vhoffset, 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 toposition: 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/streamendpoint (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:flexdirectly on a<td>(Stimme, Tags columns) broke its table-cell layout participation entirely in Chromium, rendering the cell stacked at the previous column's position regardless of table-layout mode. Moved the flex layout to inner wrapper<div>s and switched the table totable-layout:fixedwith an explicit<colgroup>for good measure.
[1.13.3] — 2026-07-06
Fixed
- Dragging a short quoted line ("»Henker«.") to split it opened the Assign-to-character popup instead — any selection under 40 characters was treated as "assign this text as a character name," even when it was clearly a short quoted line (guillemets/quotes are never part of a name). Selections starting with a quote mark now fall through to the "Split text to Unknown Speaker" button instead of being hijacked into the assign popup.
[1.13.2] — 2026-07-06
Fixed
- Character table view rendered as stacked blocks, not a table — table rows reused the
.lib-char-cardclass (for its click/voice/export wiring) whosedisplay:flex;flex-direction:columnturned every<tr>into a flex column, stacking its cells vertically instead of laying them out side by side. Reset todisplay:table-rowand 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
fieldsfilter 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-itemstyling: 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-characterssucceeded every time, but/api/voice-designwas never once called. Root cause:rehState.cast[sp].voicewas accessed unguarded right after the (multi-second, for a large cast) analyze-characters request resolved; any cast-entry change during that window threw a TypeError with nothing to catch it, silently killing the whole run with no toast and no further requests. Guarded the access and wrapped the rest of the flow in a try/catch/finally so any future failure surfaces as a toast and always re-enables the button instead of leaving it stuck with no feedback.
[1.12.98] — 2026-07-05
Added
- Character table view in Library → Characters/Cast (toggle next to "Import from SillyTavern") — one row per character with avatar, name, sex, line count, assigned voice's language, moral alignment bar, voice, tags, and check/cross indicators for whether the SillyTavern, TTS voice-design, and image prompts have been generated. Persists your choice between Cards/Table.
- Bulk voice auto-assign — a checkbox on every character card/row plus an "Auto-assign selected" button in each production's header, so you can voice-cast a batch of characters in one click instead of one at a time. Runs sequentially (not in parallel) so each assignment sees what the previous one just picked and doesn't hand out the same voice twice.
- Character tags are now visible on cards — every character is auto-tagged with the book it was extracted from (already happened silently); the cards just weren't showing it. Useful for spotting the same character recurring across different books.
- Jump-to-source links on detail fields — Backstory, Motivation, Relationships, etc. now show small numbered links next to the label when the sheet has a citation for that field; clicking one scrolls straight to the exact page + quote in "Quellen im Text" instead of making you search the full source list.
- Gender is now an extracted character-sheet field (male/female/nonbinary) — it existed in the UI already but the LLM was never actually asked for it, so every character silently showed the same default icon.
- Line-count tracking (best-effort) — casting from Read Aloud or Script Rehearsal now tallies each character's dialogue-line count into their sheet when it's available in memory, feeding the new table view's "Zeilen" column.
Fixed
- SillyTavern & Concept Art prompts still silently failed to generate even after the earlier max_tokens increase — they're the last two fields in a four-field JSON object, so any truncation always cost the same two fields regardless of the ceiling. Split into two independent LLM calls (Voice Design + Image / SillyTavern + Concept Art), run concurrently, each with its own full token budget — a squeeze in one pair can no longer cost the other pair anything.
[1.12.97] — 2026-07-05
Fixed
- German umlauts mojibake'd during live casting ("Häfen" → "Häfen") — the streaming attribution endpoint decoded the LLM's SSE response with
requests' guessed encoding, which falls back to Latin-1 when the upstream doesn't declare a charset. Forced UTF-8 explicitly. - "LLM Thinking" pane duplicated the passage text for models that ignore the reasoning-preamble instruction and stream straight into JSON. It now only shows genuine
<think>...</think>content when present, and otherwise labels the pane honestly ("Live Output (raw — no reasoning exposed)") instead of passing off raw JSON as thinking. - A−/A+ font-size buttons had no visible effect —
.ab-cv-rowhad a hardcodedfont-size: 13.5pxthat 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
assignNameexisted 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
indexOfinstead 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'sBaseHTTPMiddleware), 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 rawsendcallable — it never wraps the response the waycall_next()does, so the race is gone entirely. - Casting timeouts/405s during long runs — the streaming attribution endpoint used its own lock, separate from the blocking endpoint's. That meant a stream request and its own blocking fallback could both fire into the LLM's single processing slot at once — exactly the "ghost request" pile-up the original lock existed to prevent. Both endpoints now share one lock. Stream failures are also logged server-side now (silent before).
Changed
- The "LLM Thinking…" pane now shows real reasoning, not the JSON answer echoed back. The attribution prompt asks for a brief
<think>...</think>rationale before the JSON when streaming; the client shows only that block and stops once it closes, instead of dumping the raw in-progress JSON (which mostly just reproduces the passage text).
[1.12.95] — 2026-07-05
Added
- Watch the LLM think, live — the "LLM Reading…" card now splits into two panes the moment the model starts responding: its raw output stream (reasoning + the answer JSON as it's written) on the left, the passage it's reading on the right. Backed by a new streaming endpoint (
/api/attribute-dialogue/stream, SSE) that forwards bothreasoning_content(thinking models) andcontentdeltas; 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 —
statusTextis 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'sdata-nameattribute and corrupt the markup. All names are now matched in a single combined pass that never re-scans its own output.
Added
- Chapter headings render as headings in the casting feed — short standalone narration that reads like a title ("Prolog", "1. Kapitel", an OCR'd headline band) is now bold, larger, and centered on the paper page.
- Font-size controls in the casting toolbar — A−/A+ buttons scale the paper pages' text; the size is shared with the Script Rehearser Stage (same persisted scale), so one reading size applies across both screens.
[1.12.91] — 2026-07-04
Fixed
- No way back from Cast Characters to the casting script — the Characters/Cast view's production bar now has a "Casting" button that returns to the active casting session (via the reader's cast-restore), the pipeline stepper now also renders on the Library section so prev/next navigation works from there too, and the stepper's "Cast Characters" stop now navigates to the Characters/Cast view instead of kicking off sheet generation as a side effect of clicking a navigation element.
- Two of the four generation prompts silently came back empty — the LLM's answer was truncated by a too-small output budget (1600 tokens for four prompts, one of them multi-section), the JSON parse failed, and the UI showed success with two empty boxes. The budget is now 4096 tokens, a cut-off answer is salvaged for the fields that did complete, an all-empty response is a loud server error instead of a silent success, and a partial result names exactly which prompts are missing so you know to hit Generate again.
Changed
- "Reading PDF… page N / M" progress pill enlarged and vertically centered in the document pane (was a small pill pinned to the top edge), so long extractions have a clearly visible "still working" signal.
[1.12.90] — 2026-07-04
Fixed
- Page freezes ("Page Unresponsive") after opening a book / the casting feed — the recent "click any word to assign" feature wrapped every single word of every casting segment in its own
<span>with hover styles; at book scale (~1,500 segments) that meant ~100,000 extra DOM nodes rebuilt synchronously on every feed redraw, freezing the tab. The spans are gone: the word under the cursor is now found via the browser's native caret-position API (caretRangeFromPoint) and highlighted with a single reused overlay element — same click/hover/drag-to-assign behaviour, zero extra DOM. - Burst of identical export downloads — clicks queued up while the page was frozen could all fire at once on the export button when the tab unblocked, spawning one download + save-dialog per queued click. The export now ignores re-triggers for 2 seconds (and the freeze itself is fixed above).
Changed
- "Export cast .md" is now "Export cast .zip" — one zip bundle containing the cast as a readable Markdown script plus a Markdown sheet per character of the book (identity, appearance, personality, story, abilities, and the generation prompts), instead of a single cast file and no character sheets at all.
Added
- Generation Prompts on every character sheet — a new section in the Character Library detail view with four fold-out, copy-ready prompt boxes: Voice Design (Qwen3 TTS), Character Image (profile portrait), SillyTavern character card, and Concept Art (turnaround/model sheet). A "Generate" button fills all four in one LLM call over the character's complete profile — the previous behaviour generated voice/image prompts passage-by-passage during sheet extraction, where the model only ever saw a fraction of the character. The boxes are editable in place (autosaved like every other sheet field) and the results are included in the cast .zip export.
[1.12.89] — 2026-07-04
Added
- Quick "add alias" shortcut in the Casting sidebar — hovering a character in the "Characters found" list now reveals a small tag icon; clicking it opens a tiny popup to add an "also known as" name (e.g. "Garthai" for "Sharraz Garthai") without leaving the casting screen. Writes through the same
clUpsertthe 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, andsplitTextIntoChunks— 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 —
_abMergedSegmentbuilt a fresh segment object and never carried over.pagefrom 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
jsoncode 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
.pagefield 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
setTimeoutthat 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 callingshowReaderView()afternavTo(). 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 sharedstatusLlmTarget()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-onlyreaderState.textExtractedflag.
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 deferredsetTimeout(nav.js), which silently undid the stepper's ownshowReaderView('main')call right after it ran. Now sets the existing_readerSuppressCastRestoreflag (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-inlinebackground set tovar(--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. Addedflex-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 aReferenceErrorcaught 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 userehDbGetById(), 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-rehearserwhile 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+512vs. the oldlen(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
tagfield 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'sflex:1had 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-stepperand.reader-extract-bannerrules setdisplay: flexunconditionally, 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
pendingstate, 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.runningfalse 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:-1sentinel 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-barsetdisplay:flexunconditionally, 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 individualstatic/js/*.jsfiles, 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_secondsrequest 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.dbvia a sharedapp_statetable while continuing to mirror JSON files for compatibility. - SQLite voice library index —
/api/voicesnow 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/indexreports 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=1when 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, andfishspeech_urlare 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.jsintostatic/js/library-characters.js;library.jsnow owns only production overview, book/play cards, and cross-links. - Removed obsolete monolith — deleted the unused tracked
static/app.js; the active runtime path remainsloader.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
DocumentFragmentand avoids rebuilding filter<select>options when the option set has not changed. - Less hidden DOM work — background voice-library refreshes update data without rendering the full voice list while the Voices section is hidden.
- Faster character render wiring — replaces repeated
all.find(...)lookups with an id map and batches production card insertion.
Fixed
- Production bundle parity —
scripts/minify.mjsnow includesbenchmark-voice-picker.jsandlibrary-characters.js, matching the fallback loader order.
[1.12.52] — 2026-06-30
Fixed
- Uncaught TypeError in settings.js —
$('voice-id-input').addEventListenercrashed on load when the Clone section HTML hadn't been injected yet; guarded with?.
[1.12.51] — 2026-06-30
Fixed
- Engines not loading —
loadLocalContainers()was called once at module load before the section HTML existed; now called every time the Engines section is shown and on every tab switch (all three tabs: TTS, STT, LLM)
[1.12.50] — 2026-06-30
Added
- Skeleton loading for engine cards — the LOCAL section in Engines (TTS / STT / LLM tabs) now shows animated shimmer placeholder cards while querying Docker container status instead of a blank box
Performance
- Parallel Docker status checks —
GET /api/local-containersnow queries all 25 container definitions concurrently viaasyncio.gather + asyncio.to_threadinstead of sequentially; reduces load time from ~25× latency to ~1× (one Docker socket call worth of time)
[1.12.49] — 2026-06-30
Added
- SQLite persistence for characters and rehearsals — character library and script rehearsals are now stored in
config/tts_creator.db(SQLite) instead of browser IndexedDB; data survives browser-cache clears, works across browsers/devices, and is ready for future migration to PostgreSQL/Supabase - New REST API:
GET/POST/PUT/DELETE /api/characters/{id}andGET/POST/PUT/DELETE /api/rehearsals/{id}backed bycore/database.py - One-time automatic migration — on first load, if server DB is empty, existing IndexedDB data is automatically exported and imported to SQLite (characters and rehearsals separately)
[1.12.48] — 2026-06-29
Added
- Server-side cast persistence — after every casting chunk and after manual corrections, the full cast (segments + roster) is saved as
Scripts/cast.mdinside the book's server directory (config/reader_library/<id>/Scripts/); survives browser-cache clears and works across devices/browsers - Cross-device restore —
audiobookOpenCastViewnow has a 3-tier restore: (1) in-memory, (2) localStorage, (3) server fetch; whichever fires first wins; server draft is cached into localStorage automatically - New REST endpoints:
GET/PUT/DELETE /api/reader/docs/{id}/scripts/{name}andGET /api/reader/docs/{id}/scripts(list)
Fixed
- Casting tab didn't restore draft on click — the "Casting" sidebar nav item called
showReaderView('cast')which only shows the panel DOM; now callsaudiobookOpenCastView()which runs the full restore logic
[1.12.47] — 2026-06-29
Added
- Skeleton loading UI — the casting feed and "Characters Found" panel now show animated shimmer skeleton rows while waiting for results instead of a blank gray area
- Version shown instantly — app version in About page is read from a
<meta>tag immediately on load (no async delay); server fetch confirms it in the background
Fixed
- Progress bar hidden before casting starts — the 0% bar no longer shows in the initial "Ready to cast" state; it only appears once casting begins
- VERSION file updated — the server-side version was stuck at 1.12.37; now correctly reports the current version after container restart
[1.12.46] — 2026-06-29
Added
- Edit pencil in casting profile — pencil icon in the top-right of the character header banner; clicking it enables inline editing of all character sheet fields (appearance, personality, history, skills, relationships) with 900ms auto-save; clicking the checkmark exits edit mode
- Sources in casting profile — "Quellen im Text" section now appears at the bottom of the character profile; clicking a source page jumps to that page in the Read Aloud reader
Fixed
- Progress bar hidden after casting — the yellow progress bar and passage counter disappear automatically when the casting run is complete
- Profil button toggles — clicking "Profil" when the profile is already open closes it (returns to script); the button label flips between "Profil" and "← Skript"
[1.12.45] — 2026-06-29
Changed
- Casting character bar — clicking a character in the casting view's character list no longer hides the feed; instead a compact bar appears above the feed showing the character's avatar, name, and dialogue-line count
- All dialogue rows for the selected character are highlighted in the feed (subtle purple tint)
- Prev/next navigation — up/down buttons in the bar scroll the feed to the previous or next line for that character, with a "3 / 76" position counter
- Search within character lines — typing in the search box filters to matching lines and scrolls to the first hit; pressing Enter advances to the next match
- Open profile button — the "Profil" button in the bar hides the feed and opens the full character sheet (voice assignment, alignment, history, etc.); the back button restores the feed and re-applies highlights
- Re-clicking the same character in the list closes the bar
[1.12.44] — 2026-06-29
Added
- Character detail in the casting view — the "Characters Found" list in the Read Aloud casting panel now shows colored letter-circle avatars (matching the Library view colors) instead of small pill chips; clicking any character hides the cast feed and opens their full character detail (header, voice assignment, alignment, appearance, personality, history, skills, relationships) in the feed area; a "← Zurück zum Skript" button at the top restores the feed
- Voice assignment from casting panel — Auswählen, Automatisch, and Online suchen voice buttons work inline in the character detail panel; after assigning, the panel refreshes with the new voice
[1.12.43] — 2026-06-29
Fixed
- Character detail page scrolls properly —
.lib-char-pagenow has a fixed viewport height (100dvh - 148px) so the main content panel and the character sidebar each scroll independently; sidebar no longer floats outside or breaks sticky - Sidebar clearly separated — 3 px border + inset box-shadow replaces the thin 1 px line; sidebar is now 240 px wide with more padding
- Source quote → reader navigation always works — clicking a source now always navigates to
s-readerfirst; if the book is already open it scrolls to that page div (or finds the first sentence on that page viareaderJumpTo); only shows a "reopen book" toast if neither source is available - Auto-assign voice respects German gender terms — gender detection now handles both English (
female/male) and German (weiblich/männlich) by checkingstartsWith('w')as female in addition tostartsWith('f'), preventing female voices from being assigned to male characters whose gender field was written in German
[1.12.42] — 2026-06-29
Changed
- Character detail is now a full page, not a popup — clicking a character card opens a two-panel detail page inside the Library section; center pane shows the full character sheet, right sidebar lists all characters in the same production sorted by appearances; "← Alle Charaktere" back button returns to the card grid
- All character fields are inline-editable — every text field (name, aliases, archetype, physical, backstory, relationships, etc.) is
contenteditablewith auto-save (900 ms debounce after each keystroke) viaclPut; the moral alignment bar is now a draggable<input type="range">slider - Source quotes in character detail are clickable — clicking a "Quellen im Text" source entry navigates to that page in Read Aloud (scrolls the reader to that page div); shows a toast if the book is not open yet
[1.12.41] — 2026-06-29
Changed
- Portrait-style character cards — cards are now vertical with a colored gradient banner at the top holding the 72 px avatar; name + archetype centered below; snippet, alignment bar, and relationship dots fill the body; compact voice row with "Auswahl" / "Auto" at the bottom; grid uses
minmax(200px, 1fr)so more fit per row - Voice assignment moved to top of character detail modal — the "Stimme" row is now the first thing below the header, so voice picking is always visible without scrolling
- Quellen (sources) section added at bottom of detail modal — shows verbatim text quotes with page number and context label from the character sheet generation; gives evidence for each character detail
- Stronger German language enforcement for character sheet generation — system prompt now starts with a bilingual ⚠️ SPRACHE / LANGUAGE block listing every field that must be in German; the user-message reminder also now explicitly says no field may be in English
Fixed
- Existing character data generated in English can be refreshed by re-running "Cast Characters" from the production header — the language instruction is now strong enough that even stubborn models should comply
[1.12.40] — 2026-06-28
Fixed
- Character detail modal had transparent background —
var(--card)is not defined; replaced withvar(--surface)andvar(--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 —
_charRelsHtmlcalled.toLowerCase()on therelationshipsfield 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 inlibrary.jsfor all rendered fields, and_clSanitize()incharacters-library.jsapplied 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 thehiddenattribute, sodone()callingov.hidden = truehad no effect. Fixed by usingov.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 (
imagefield) and survives re-generation. The letter-avatar falls back when no image is uploaded. Overlay cards show an 80×80 avatar beside the name; library cards replace the coloured initial dot with the photo.
Fixed
- "Passage N failed: Error" — the generic error message was caused by the JS not reading the HTTP status or response body before throwing. Now shows
HTTP 502: LLM character-sheet generation failed: <reason>(or JSON parse error if the server returns non-JSON). Also logs the full error to the console for debugging. - Context window overflow on early passages —
csExistingSummarywas 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
csForReaderseedscsGeneratewith_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_scoreposition. 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 viaString()as[object Object]. Added_csStr()sanitiser that joins arrays with ", " and falls back toJSON.stringifyfor any other non-string value; applied at both initial insert and merge incsMerge.
[1.12.33] — 2026-06-28
Fixed
- Blue box after "Recast Unknown → no unknown found" —
audiobookCastView()setpanel.style.display = 'flex'as an inline style. WhenshowReaderView('main')later setcast.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-inlineCSS class already definesdisplay: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 definessplitTextIntoChunks) 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.jsis 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.pngcards with the JSON embedded in a PNGtEXtchunk (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_voiceso a round-trip preserves the casting.
- Import accepts V1 (flat JSON), V2 (
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:0but 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-hiddenalso setsdisplay:none.
[1.12.26] — 2026-06-28
Fixed
- Sidebar blue highlights on wrong items — "Language Models", "Books", "Stage" were hardcoded
is-activein HTML and stayed blue even when on a different section. Added CSS rule[data-nav-section]:not(.active) + .nav-tree .nav-tree-item.is-activeto 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.jsandnav.jsare 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.jsfirst (one request instead of 21 individual files). Falls back to per-file loading only if the bundle is missing. - Eliminated
/api/versionblocking round trip — version string is read from the already-loadedstyle.csslink tag (synchronous, zero latency). Fetching/api/versionwas 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:nonerule. When the audiobook cast panel is shown and JS setsreader-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 tomdi mdi-chevron-downMDI icon class, consistent with the rest of the UI. - Sidebar sub-item highlights showing for non-current sections — added CSS sibling rule so
is-activesub-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 beforenav.jsexecutes on slow/interrupted loads. An inline stub now handles the click immediately (reads/writesttsvc_treesin localStorage, toggles CSS classes directly);nav.jsoverwrites 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
sessionStorageafter 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()calledrenderSettingsAbout()unconditionally but that function lives inconversation.js(batch E, loaded after init). Guard added withtypeofcheck; the About section still renders when opened (nav.js already guards the same call). - Duplicate element IDs —
s-library.htmlCharacters panel duplicatedcl-book-filter,cl-search,cl-gridfroms-characters.html, causinggetElementByIdto 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 assignmentwindow.loadVoiceLibrary = loadVoiceLibrary.
[1.12.17] — 2026-06-28
Fixed
- Settings load crash —
loadSettings()used direct.valueassignments on elements that no longer exist after the Library restructure; all now use null-safesv()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.jsonviaPOST /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.30xcell 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 of1.3×) in the SPEED column and live status line — more precise RTF comparison across voices. - Sorting by Speed now sorts by the RTF factor (faster voices first) instead of by raw synthesis time — voices with a higher
×multiplier rank higher regardless of how long the benchmark sentence was.
[1.12.8] — 2026-06-27
Changed
- SOURCE column auto-derives script name for Rehearser clones: voices cloned from a Script Rehearsal now automatically display the script title (e.g.
Script,her) in the SOURCE column, extracted from the note field (Rehearser · <title> · <character> — …). No manual entry needed; setting an explicitoriginstill 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-audioautomatically showfish-audioas 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-labelspan showed nothing before). - Active section in the sidebar now gets a blue background highlight + right-border accent (
.nav-tree-item.activewas 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
cardcollapse 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:
curGenderwas referenced before itsconstdeclaration insideselectVoice(), 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
speedparameter 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
\fmarkers 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:11434LLM 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 configuredllm_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
Escapekey, so you can easily dismiss it if you click "Assign" by accident.
Changed
- Audiobook casting — Narrator quick-pick: The
📖 Narratorrole 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.pyfor 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: centerwithmargin: 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_01showed as01), 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.0and a stale changelog: the deployment stack wasn't mountingVERSION/CHANGELOG.md, so the container had no live version file (fell back to0.0.0) and served the image's baked changelog. Both files are now bind-mounted indocker-compose.ymlandportainer-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-seedproxy, modulestatic/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.ptand 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 endpointPOST /api/character-sheets, modulestatic/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-dialogueand modulestatic/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-previewpipeline — no backend changes. New filesstatic/sections/s-reader.htmlandstatic/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>/withmeta.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:
splitTextIntoChunksonly 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 movedgeneration.jsfrom deferred batch E into the main feature batch sogenerateChunkedTtsis 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 viaI18N_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 indocs/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-visibilityvirtualization for long lists; lazy-loaded images; Rehearser caps decoded-PCM memory to a sliding window (fixes iPhone crashes); leakedAudioContextclosed; 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 (theinstructfield 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/CONTINUEDmarkers 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 viaFISHSPEECH_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(thenfishspeech) 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 —
narratorVoicenow stays in sync with the narrator cast row. - About → Changelog was empty —
CHANGELOG.mdis 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
localStoragefor 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
tagvalue 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-ratioon a flex child (unreliable in all major browsers) with thepadding-bottom: 150%wrapper trick, guaranteeing a correct 2:3 poster ratio. - Rehearser TTS backend "No backend available" —
refreshRehBackendsnow triggers the global backend probe if_ttsBackendsis empty, and registers a_ttsRefreshHookso 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 —
MediaRecorderrequests 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
AnalyserNodeRMS 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, writesVERSION, 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/.htmlfiles are staged butCHANGELOG.mdorVERSIONare not. Runbash scripts/install-hooks.shafter 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.ymlupdated with absolute host paths. - Server writes a rotating log file —
RotatingFileHandlerwritesINFO-level and above to./logs/app.log(rotates at 5 MB, 3 backups).
Performance
- Skeleton loading view —
index.htmlshows 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.jsinto 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_contentfor think-only responses; error message hints to add/no-thinkto 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
audioform field toOptional[UploadFile] = Noneso text-only turns don't require audio. - Conversation input bar hidden when mic unavailable — warning box moved
inside
conv-chat-windowso it never pushes the input bar off-screen. - Browser caches old section HTML —
loader.jsappends?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_BLOCKEDblocklist (/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_DIRvia.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 —
_registrychanged todict[str, tuple[Path, float]]._registry_gc()evicts entries older thanTEMP_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 —
VERSIONfile at repo root; read bycore/constants.__version__and surfaced viaGET /api/version. Displayed asv1.1.0in 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/turnnow accepts an optionaltextform 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-networkicon) when already connected and toggles back on click. State persists inlocalStorage. - 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
.activehighlight 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 samedc-controls-rowstructure as the dynamic Docker stack cards. The existing.llm-local-pingbutton 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.pyBoot-time env defaults, path constants, version, log buffer registry.pyTTL-based temp file registry validation.pyURL validation, SSRF guard, path safety docker_client.pyRaw Unix-socket Docker HTTP client config.pySettings load/save/normalize, backend URL resolution routing.pyTTS route rules load/save/resolve, language detection audio.pyAudio conversion, normalisation, auto-trim scoring voice.pyVoice metadata, backup management, benchmark helpers presets.pyVoice Design preset load/save, virtual voice resolution tts_helpers.pyTTS request helpers, streaming, per-backend logic routes/admin.pyIndex, favicon, browse-dirs, robots, version settings.py/api/settings, routing rules, logs, design presetslibrary.pyAll voice CRUD, upload, save, normalize, export/import stt.py/api/transcribe*,/api/stt-backendssources.pyVoice scraping, proxy-audio, yt-dlp download docker.py/api/local-containers/*,/api/probe-urltts.pyTTS preview, streaming, voice design, /v1/*, backendsconversation.pyRefine-text, effects, export/import, speak, MCP, conversation Dockerfileupdated withCOPY core/ core/andCOPY routes/ routes/.docker-compose.ymlupdated with./core:/app/core:roand./routes:/app/routes:ro. -
Frontend refactor —
app.js(8 744 lines → 16 modules) — split intostatic/js/withloader.jsloading them sequentially in dependency order:Module Lines Responsibility utils.js364 Core helpers: $,toast,escHtml, theme, language/flag, picker, tabsvoice-inspector.js397 3-pane voice workbench voice-sources.js277 External voice source scraping UI integrations.js211 Code snippet generation (SillyTavern, Open WebUI, HA, curl, MCP) routing.js542 TTS routing rules editor settings.js385 loadSettings,applyAndSaveSettings, settings panelvoice-clone.js774 WaveSurfer, drop zone, mic recording, trim, voice design voice-library.js2654 Full voice library: list, CRUD, benchmark, normalize tts-preview.js528 TTS preview, fetchTtsPreviewBlobbenchmark.js218 Performance + batch benchmark stt.js287 STT→TTS playground, refreshSttBackendsinit.js49 App bootstrap engines.js625 ElevenLabs browser, custom engine cards, Docker management ai-backends.js520 AI backend cards, LLM snippets, initStaticDockerManagementgeneration.js393 WAV merge, chunked TTS, history, playlist, audio effects conversation.js520 Conversation playground, LLM refinement, import, About
Fixed
chrome://flags/…URL unreadable in mic-blocked warning — the globalcode { 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 addone-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/speechand/v1/audio/transcriptionsfor 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_DEFAULTmapping 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.