Table view was rendering as stacked blocks instead of columns: rows
reused .lib-char-card for its event wiring, but that class's
display:flex;flex-direction:column turned every <tr> into a flex
column. Reset to display:table-row and stripped the leaked-in card
chrome.
Split /api/character-generate-prompts into four independent per-field
LLM calls (from two paired calls) and added a `fields` filter, so the
UI can offer one Generate button per prompt box instead of a single
button that always regenerated all four - cheaper, and further
shrinks each response to reduce truncation risk.
Added a Sort dropdown (Role/Alphabet/Lines/Gender/Voice assigned) to
the Characters/Cast list, persisted like the Cards/Table toggle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Table view: one row per character (avatar, name, sex, line count, voice
language, alignment, voice, tags, prompt-availability checks), toggled
next to the card grid and persisted.
Bulk voice auto-assign: checkbox per character + "Auto-assign selected"
per production, sequential so later picks see what earlier ones just
took (avoids duplicate voice assignments).
Character tags (auto-set to the book of origin) are now visible on
cards - the field always existed, cards just never rendered it, so a
character recurring across books had no visible link between records.
Detail fields (Backstory, Motivation, etc.) now show small numbered
links to their exact source citation when the sheet has one, instead
of making the reader search the full "Quellen im Text" list.
Added gender as an actual extracted character-sheet field - the UI
already had a gender icon but the LLM was never asked for the value.
Fixed SillyTavern/Concept Art prompts still coming back empty despite
the earlier token-budget increase: they're the last two fields in one
JSON object, so truncation always cost the same two regardless of the
ceiling. Split into two independent, concurrent LLM calls instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The streaming attribution endpoint decoded the LLM's SSE response with
requests' guessed encoding (Latin-1 fallback when no charset is declared),
mangling every German umlaut. Forced UTF-8 explicitly.
The "LLM Thinking" pane duplicated the passage text for models that
ignore the <think> instruction and stream straight into JSON - it now
only shows real reasoning when present, and otherwise labels raw output
honestly instead of passing it off as thinking.
Also fixed three UI bugs found while testing a live multi-hour cast:
- A-/A+ font buttons had no effect (a hardcoded font-size on .ab-cv-row
always overrode the CSS variable they set).
- Typing a name + Enter in the "Assign to" popup (and drag-to-assign,
which reuses it) silently did nothing after the first cast/recast run
in a session - the popup is a page-lifetime singleton but its input
handlers closed over the first run's now-stale assignName/closePopup.
Every popup open now repoints them at the current run.
- "Split text to Unknown Speaker" split at the wrong spot when the
selected phrase repeated earlier in the same paragraph (indexOf found
the first occurrence, not the dragged one). Now uses the exact DOM
range offset instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The static-asset caching middleware used BaseHTTPMiddleware, which has a
known Starlette bug: a client disconnecting mid-StreamingResponse (the new
live-attribution SSE stream hitting its idle timeout) raced its internal
task group and raised "RuntimeError: No response returned", crashing that
request. Rewritten as plain ASGI middleware that only touches headers via
the raw send callable, removing the race.
Also found the real cause of the casting timeouts/405s: the streaming
attribution endpoint had its own lock instead of sharing the one the
blocking endpoint already used to serialize on the LLM's single slot -
letting a stream call and its own blocking fallback fire concurrently,
exactly the ghost-request pile-up that lock was built to prevent. Unified
onto one lock and added server-side logging for stream failures.
The "LLM Thinking" pane now shows the model's actual <think> reasoning
instead of the in-progress JSON answer echoed back at the user.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The LLM Reading card splits into thinking-stream (left) and passage
(right), fed by a new SSE endpoint that shares prompt-building and
parsing with the blocking one and falls back to it on any stream
failure (inactivity timeout, not overall). The deterministic resolver
no longer invents speakers from scenery nouns (PLATZ/GESICHTER/
KLEINIGKEIT) — person-noun whitelist plus a clause-subject pattern —
and gains the impersonal post-quote formula and strict two-person
alternation with colon/page/window guards. All reported failure cases
verified against the exact book sentences.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The saved casting prompt was the 2nd-quality verification prompt, so
first-pass attribution ran with the wrong job description and none of
the deduction rules — and the client-side prompt migration had no
anchor to upgrade. The server now appends the rules to any prompt
lacking them, and the saved prompt was reset to the default (backed up
to config/audiobook_prompt.backup.txt). Also: single-pass combined name
regex (a shorter alias could match inside a longer name's data-name
attribute and leak raw style="..." into the feed), stopword filter so a
comma-split alias like "Die, die den Vampir verließ" can't underline
every article, heading-like narration renders bold/centered, and A-/A+
font controls in the casting toolbar sharing the Rehearser Stage scale.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Characters/Cast gains a "Casting" button back to the active casting
session, the pipeline stepper renders on the Library section, and the
stepper's Cast Characters stop navigates instead of side-effect-running
sheet generation. Prompt generation: 4096-token budget (1600 truncated
the four-prompt JSON so two fields silently arrived empty), truncated
answers salvage completed fields, all-empty responses fail loudly, and
partial results name the missing prompts. The PDF-extraction progress
pill is enlarged and vertically centered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-word <span> wrapping behind "click any word to assign" created
~100k DOM nodes at book scale and froze the tab on every feed redraw;
replaced with native caretRangeFromPoint word detection plus a single
reused hover overlay — same UX, zero extra DOM. Export button gained a
2s re-entry guard (queued clicks during a freeze fired as a download
burst) and now delivers one zip: the cast script in Markdown plus a
sheet per character. Character detail view gains a Generation Prompts
section — four fold-out copy boxes (Voice Design, Character Image,
SillyTavern card, Concept Art sheet) filled by one LLM call over the
full profile via the new /api/character-generate-prompts endpoint.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add a 6-stage pipeline stepper (Source -> Cast Audiobook -> Cast Characters
-> Script Rehearser -> Generate MP3s -> Audiobook) with direct, non-destructive
jumps between stages and a prominent guided-tour look
- Split PDF import into an explicit "load" then "Extract Text" step, with
in-browser OCR (Tesseract.js, vendored) to recover chapter headlines baked
into a PDF as images instead of real text
- Fix casting feed silently merging pages after leaving/returning: segments
now carry their own page number instead of re-guessing it from text
- Fix excessive "Unknown" speaker attribution: restore the attribution LLM's
output token budget, which had been cut roughly in half and was truncating
dialogue-dense passages
- Fix Theater Play library cards failing to open (dead pre-migration
IndexedDB API calls, missing section navigation)
- Fix bulk "Set tag" wiping a voice's existing tags instead of adding to them
- Start merging Casting's feed with Script Rehearser's Stage UI: collapsible
character sidebar, shared "paper" page styling, inline text editing
- Fix a performance regression from that merge (per-row listeners on every
redraw) by moving to event delegation
- Various layout/clutter fixes: hide reader chrome until a document is
loaded, collapse secondary settings by default, fix overlapping toolbar
icons, fix duplicate "opening" notifications
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Exposes the `speed` parameter (0.5–2.0) on the TTS `/v1/audio/speech`
request so audio is generated at the target tempo natively via the
faster-qwen3-tts backend rather than post-processing.
- Backend: `/api/tts-preview` now extracts and forwards a `speed`
override (clamped 0.1–4.0) through the same extra-params mechanism
already used for seed/temperature; backends that reject it fall back
cleanly via `_post_tts_with_fallback`.
- Try it out: "Native Speed" number input (0.5–2, step 0.05) added to
the text card; value persists in localStorage per browser; passed as
`extra` through `createTtsAudioSource` and `generateChunkedTts`.
- Read aloud: "Native Speed" control added to the generation controls
row alongside seed/temperature; included in `readerGenParams()` and
saved/restored with library documents (each book tracks its own speed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a persistent, book-scoped Character Library (new Characters section,
IndexedDB) that auto-fills from Character-sheet analysis with editable cards.
Enrich extraction with six narrative fields (backstory, relationships,
motivation, fears, mannerisms, voice/speech) plus the greyscale Good↔Evil
alignment bar, arc arrow, and 5-area Deep Analysis. Add ⋯ separators between
non-contiguous passages in Recast unknown, and harden dialogue attribution
against hallucination with same-language emotion tags.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read Aloud (new "Vorlesen" tab):
- PDF (real page render + overlay highlight) / TXT reader with live word
highlighting, voice + speed, per-sentence synthesis-state colours, zoom
(fit-width/height, two-page, ±), resume, and a server-side book library
(syncs across devices; per-unit MP3 audio fetched on demand).
Book -> multi-speaker audiobook:
- "Cast as audiobook" attributes dialogue to characters via the LLM
(guillemet/quote-style aware, turn-taking, recent-context), with a
deterministic speech-tag fallback. Editable preview, non-blocking live
casting panel, then auto-saved as a reopenable Script Rehearser play.
- Audiobook export: synthesise every cast line -> one MP3 per chapter.
Character sheets:
- LLM-extracted, self-filling RPG-style sheets (with page+quote sources)
in both Read Aloud and the Rehearser.
Also: MP3 storage + per-page/sentence export, voice-library "Precompute
embeddings" pre-warm, German "Vorlesen" i18n + flag language toggle,
large-PDF performance (lazy raster, buffer/canvas eviction, yielded parse),
and the Seed Finder changelog entry.
New: routes/reader.py, POST /api/attribute-dialogue, POST /api/character-sheets,
static/js/{reader,audiobook,character-sheets}.js, static/sections/s-reader.html.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a collapsible "🎲 Seed Finder" panel to the voice inspector pane.
Select a voice, expand the panel, set a seed range (default 1–15), and
click Run to generate audio samples with different seeds.
Each result row has Play and "Use this seed" buttons. "Use this seed"
saves the chosen seed to the faster-qwen3-tts voices.json via the new
/api/tts-voice-seed proxy endpoint (which calls /voice-seed on the TTS
server). The saved row gets a ✓ marker.
Test sentence auto-selects DE/EN mixed or EN-only based on voice name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- voice_clone and streaming now return active library voices instead of
probing upstream /v1/models (which only exposed 8 model entries)
- Accept POST on /v1/audio/voices for clients that probe with POST
- Fall back to first active voice when voice field missing in /v1/audio/speech
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Script Rehearser (new feature)
- New section s-rehearser.html + rehearser.js + nav/loader wiring
- Phase 1: paste/upload script (.txt), auto-detect characters from
'CHARACTER: dialog' or ALL-CAPS screenplay format
- Phase 2: assign a TTS voice per character, or mark 'I play this'
- Phase 3: step-through rehearsal — synthesizes other characters via TTS,
shows level-meter + oscilloscope for your own lines, records them from mic
- Phase 4: session summary with per-line audio playback + download
## Connect Apps
- Removed duplicate standalone MCP/speak/hotkey full-width cards
- Kept the integration-grid cards (they use the real server URL from JS)
- Added Global Hotkey Daemon as a proper integration card with snippet-hotkey
populated by integrations.js (uses proxyBase URL dynamically)
## About page
- GET /api/changelog endpoint reads CHANGELOG.md and returns it as text
- Collapsible 'Changelog' <details> card fetches and displays it lazily
## Try It Out
- Reorganised into three cards: Voice & backend / Text to synthesize / Generate
- Backend help panel moved below the voice row (not in the same flex row)
- Style instruction field gains a dynamic badge ('style-aware ✓' / 'weak style')
and a yellow warning when a non-style-aware backend is selected while the
field is filled — wired to both backend-select change and input events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sentence text in typing bubble:
- Backend sends 'text' field with each audio SSE event (the sentence
being synthesised)
- Frontend audio queue stores {url, text} pairs
- playNextAudio() writes the sentence text into the '...' typing bubble
when LLM tokens haven't arrived yet (convCurrentSentenceBubble)
- convCurrentSentenceBubble cleared as soon as first LLM token arrives
so normal streaming takes over seamlessly
VAD noise fixes:
- VAD_THRESHOLD: 0.01 → 0.02 (background noise no longer counts as speech)
- VAD_MIN_REC_MS: 400 → 800ms (8/10s wait before silence detection starts,
gives user time to begin speaking without initial noise triggering send)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Speech gate (silence detection):
- vadHadSpeech flag: VAD auto-stop cancels without calling STT when no
speech was detected (fixes "[STT] No speech detected → gibberish" loop)
- cancelNextBlob flag: onstop skips processBlob when VAD cancels silently
- vadLastVoiceMs: gates preview transcription on actual detected speech
(prevents "reich" hallucination on initial silence chunks)
Hallucination filter:
- Client: HALLUCINATION_RE strips known Whisper phantoms from preview
- Server: _is_hallucination() in generate() treats "reich" / "danke" /
"thank you" etc. as "No speech detected" → never reaches LLM
Latency:
- VAD_SILENCE_MS: 1500 → 1000 ms (sends 500 ms sooner per turn)
- VAD_MIN_REC_MS: 500 → 400 ms
- MediaRecorder timeslice: 2500 → 1500 ms (preview text updates faster)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace serial LLM-wait-TTS with overlapped execution:
- LLM streams via background thread → asyncio.Queue (non-blocking event loop)
- _sentence_split() detects sentence boundaries in the token stream
- asyncio.create_task fires TTS for each sentence immediately — TTS for
sentence 1 runs while LLM is still generating sentences 2, 3, …
- Audio chunks stream to frontend in order as each task completes
- Time-to-first-audio drops from (LLM total + TTS total) to
roughly (LLM time-to-first-sentence + TTS latency for one sentence)
Frontend audio queue:
- enqueueAudio() / playNextAudio() chain multi-chunk responses seamlessly
- clearAudio() stops playback and cancels queue on new turn or mic click
- scheduleAutoMic() waits for queue to drain before restarting mic
- Error paths clear the queue to avoid stale audio playing after failure
Also fix missing contextlib import (silent bug when audio temp files
needed cleanup in the STT path).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Perceived startup speed:
- index.html: animated shimmer skeleton (header + toolbar + 8 voice cards)
visible immediately; fades out when loader.js finishes
- WaveSurfer (57KB) and MDI icon font (394KB woff2) now served from
static/vendor/ — removes 3 render-blocking external requests from <head>
- Flag-icons CSS loaded async (rel=preload onload trick) — non-blocking
loader.js:
- Fade out skeleton + remove from DOM (300ms transition)
- Reveal page-sections after JS finishes loading
Conversation playground:
- Qwen3 thinking mode fix: fall back to delta.reasoning_content when
delta.content is empty so think-only LLM turns produce visible output
- Better error message with /no-think hint when LLM returns empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_AUDIO_EXTS lives in core.voice, not core.audio. The dead alias
'_VOICE_AUDIO_EXTS' was never used anywhere in the file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UploadFile | None = File(None) with from __future__ import annotations
caused FastAPI to still treat audio as required when omitted.
Changed to Optional[UploadFile] = None (no File() wrapper) so the
field is genuinely optional for text-input turns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add VERSION file (1.1.0) at repo root
- core/constants.py: expose __version__ read from VERSION file
- routes/admin.py: GET /api/version endpoint returns {version}
- Settings → About: display "v1.1.0" next to app name via /api/version fetch
- CHANGELOG.md: full rewrite following Keep a Changelog + Semantic Versioning
- [Unreleased] staging section at top
- [1.1.0] 2026-05-29 — security, perf, refactor, UX changes from this session
- [1.0.0] 2026-05-28 — all pre-session features documented
- Compare links at bottom pointing to GitHub
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Frontend:
- Add pill-shaped text input + send button (→) to the left of the mic button
- Enter key or → click sends text directly without recording audio
- Input is disabled while a turn is processing; cleared on submit
- Welcome message updated to mention both input methods
- New CSS: .conv-input-bar, .conv-text-row, .conv-text-inp, .conv-send-btn,
.conv-divider (visual separator between text and mic sections)
Backend:
- /api/conversation/turn: audio is now optional (UploadFile | None)
- New text form field — when provided, STT step is skipped and text is
used as the transcript directly; SSE emits transcript event with stt_ms=null
- Raises 400 if neither audio nor text is supplied
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>