- 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>
Storage: IndexedDB ('reh-library' DB, 'rehearsals' store).
Blobs (recorded mic audio) stored natively; no base64 overhead.
Library panel (Phase 1):
- List of saved rehearsals sorted by last update
- Each row: character-colour avatars, title, progress bar,
line count, recorded-clip count, date
- Active record highlighted in accent colour
- Open / Export .reh / Delete per-row buttons
- Import .reh button at the top
Transport bar (Phase 3):
- Save button (updates existing record if savedId set, else creates new)
- Export button (downloads current state as .reh JSON file)
Phase 4 (session complete):
- Save to library and Export .reh buttons
Data format (.reh file):
- JSON with version=1, title, script text, cast map, backend, lineIndex
- clips serialized with audio as base64 strings (mime + b64 fields)
- clipsFromJson restores Blob objects on import
Behaviour:
- Parsing a brand-new script resets savedId → safe to save as new record
- loadRecord() restores script, cast, lineIndex, clips and jumps to Phase 2
- renderLibraryList() called on init, after save, after delete, after import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- A4 paper page (white, serif font, shadow) renders the full script at once
so you can read ahead while rehearsing
- Each dialog block shows: character avatar (voice library photo or initial
letter), character name in their colour, TTS/Me badge, and dialog text
- Active line gets a blue left-border highlight and auto-scrolls into view
- Transport bar (sticky, above the script):
- Cast strip: all character avatars at a glance
- ⏮ Prev / ▶ Play all / ⏹ Stop / ⏭ Next / 🔁 Repeat
- Progress bar + line counter
- Auto-play: TTS lines synthesize, play, auto-advance; 'me' lines pause
and slide up a sticky recording overlay at the bottom of the page
- Recording overlay shows the line to speak, oscilloscope + meter,
Record / Stop / Keep & continue / Re-record / Skip
- voice-library.js now exports window._voices so the rehearser can resolve
voice IDs to has_picture flags and fetch /api/voice/picture/{id}
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>
Loader:
- Post-init modules (engines, ai-backends, generation, conversation) now
load AFTER the skeleton is removed instead of before. The UI is visible
~500 ms sooner on average; those four modules load while the user is
already browsing Voices / Clone / Design.
- Removed the sttReady event approach that was triggering a duplicate
/api/stt-backends call; init.js already populates all STT selects once
on startup.
Skeleton:
- Replaced the card-grid placeholder with a two-column workbench skeleton
(voice list rows on the left + inspector placeholder on the right) that
matches the real My Voices layout.
Connect Apps:
- MCP Server, /speak REST endpoint, and Global hotkey daemon sections
moved from Settings → About to Connect Apps, where they belong.
- About page now has GitHub + Releases links instead.
Clone section:
- Added a hint note beneath the sample-text textarea reminding the user
to replace the placeholder name (Sam / Alex / Marco …) with their own.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: VERSION was not volume-mounted, so the server always reported
1.2.0, causing browsers to serve 1-year-immutable cached JS even after
code changes.
Fixes:
- docker-compose.yml: add ./VERSION:/app/VERSION:ro volume mount.
After `docker compose up -d`, the server reads the current VERSION file
and JS is cache-busted by the correct version string.
- loader.js: append session timestamp to _appVersion so every page load
generates a unique JS URL. JS is always fresh regardless of whether
VERSION is current, at the cost of one network round-trip per file per
session (acceptable for a local tool).
- s-clone.html: embed EN default text directly in the textarea so the
field is never empty even before JS runs.
- voice-clone.js: remove the 'skip if already filled' guard in
initCloneSampleText so navigating back always resets to the language
text; call refreshSttBackends on load with sttReady event fallback.
- stt.js: dispatch 'sttReady' event after all STT listeners are wired.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JS files are cache-busted by app version (?v=1.3.0), so browsers that
served stale v1.2.0 scripts will now fetch the updated voice-clone.js,
stt.js, style.css, and nav.js.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. Sample text: expose initCloneSampleText as a window function and call
it from nav.js runSideEffects when the clone section is activated,
ensuring the textarea is always populated even if the IIFE ran before
the element existed.
2. Better sample texts: all 8 languages rewritten to ~38 words / ~15 s,
first-person, phonetically rich, proper Unicode diacritics.
3. Live mic monitor: a level-meter (18-bar) + scrolling oscilloscope
canvas (ring-buffer, 300 px, colour-coded) added to the microphone
card. "Check level" / "Stop monitor" buttons start/stop it
independently; clicking Record starts it automatically.
Uses raw mic constraints (no echo-cancel / AGC) for cleaner voice clone
audio. Mic gain slider and dB readout included.
4. Recording quality: MediaRecorder now requests audioBitsPerSecond:256000
in both voice-clone.js and stt.js.
5. STT engine picker: Recognition engine <select> + Refresh button added
above the Auto-transcribe button in Step 3. refreshSttBackends() now
syncs both stt-tts-stt-backend and clone-stt-backend. The transcribe
call passes the chosen backend to /api/transcribe.
6. File input: explicit extension list added to accept= for OGG/OPUS.
7. CSS: .mic-live-wave style added (dark/light theme variants).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the 'Read aloud' sample-sentence box back to the microphone card
in the Clone a Voice tab. Language switcher covers EN/DE/IT/ES/FR/PT/NL/PL
with phonetically diverse sentences (same as the My Voices panel).
Uses the existing .sample-read-box / .sample-sentence CSS so it looks
identical to the equivalent panel in the voice library.
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>
Previously all 11 section HTMLs had to complete before JS loading began.
Now only the active section loads first (needed before JS runs so event
listeners find their DOM elements). The other 10 sections fetch in parallel
with utils + settings; an explicit await before the 9-module feature batch
guarantees all section HTML is injected before any feature module sets up
its DOM listeners. On a local server the HTML files finish well before
settings.js completes, so the await is typically instant.
Sequential steps cut from: sections(11∥) + utils + settings + features + ...
to: section(1) + max(utils+settings, sections(10∥)) + features + ...
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>
Conversation playground:
- Live speech preview: MediaRecorder sends accumulated audio to
/api/transcribe-bytes every 2.5 s; interim Whisper result shown in
the text input field while recording. Web Speech API tried first as
a faster path when available (HTTPS/localhost).
- VAD auto-stop: AudioContext AnalyserNode measures RMS every frame;
auto-stops after 1.5 s silence with a visible countdown. Auto-stop
toggle to revert to click-to-stop.
- Hands-free mode: mic auto-restarts after the agent finishes speaking
via audio.ended event + generation-counter cancellation. Hands-free
toggle (on by default) to disable.
Navigation:
- Persist active section and sub-page in localStorage; hard-reload
returns to the same page instead of always jumping to My Voices.
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>
config/ and logs/ are now host directories visible on the filesystem.
- docker-compose.yml: ./config → /home/app/.config/tts-voice-creator:rw
./logs → /logs:rw
Named volume tts-voice-creator-clone-and-design-2 removed.
- portainer-stack.yml: same change with absolute host paths.
- server.py: RotatingFileHandler writes INFO+ to /logs/app.log
(maxBytes=5MB, backupCount=3). Falls back gracefully if /logs
is not writable.
- .gitignore: track config/ and logs/ dirs via .gitkeep but exclude
settings.json, *.log and backups from version control.
Benefits:
- Settings and logs are human-readable on the host at any time
- Survives docker-compose down -v (was lost with named volume)
- Easy backup: cp -r config/ logs/ to any destination
- Can edit settings.json directly if needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously: container names and dynamic card URL overrides only
written to localStorage — lost when accessing from a different
browser or after clearing browser data.
Now:
- engine_container_names added as persisted settings key (dict)
- All container name inputs tagged data-cn-key for loadSettings() lookup
- loadSettings() restores container names + dynamic URLs from server
- settings.js exposes _saveEngineContainerNames() and
_saveEngineLocalUrls() globally so engines.js / ai-backends.js
can call them on every input event
- Static cards (ai-backends.js initStaticDockerManagement): reads
saved name from engine_container_names first, localStorage fallback
- Dynamic Docker stack cards (engines.js): same priority for both
URL and container name; both write to server on change
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>
- Move mic-blocked warning into conv-chat-window (prepend) so it scrolls
with the chat instead of pushing the input bar off-screen
- Remove min-height:400px from conv-chat-window; give conv-chat-panel a
viewport-relative height so the input bar is always visible at the bottom
- loader.js: append ?v=<timestamp> to section fetches to bust browser cache
(was serving stale s-conversation.html after updates)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scripts/release.py:
- --patch/--minor/--major flag bumps VERSION file
- Renames [Unreleased] → [x.y.z] — date in CHANGELOG.md
- Inserts fresh [Unreleased] section + correct compare links
- Commits + creates annotated git tag
- --dry-run flag for preview without writes
- Prints git push + GitHub Releases URL on completion
scripts/hooks/pre-commit:
- Warns (exit 0, non-blocking) when source files are staged but
CHANGELOG.md or VERSION are not staged
- Already installed in .git/hooks/
scripts/install-hooks.sh:
- One-liner to install hooks after a fresh clone
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>
Engine cards (all TTS + STT, dynamic Docker stack + static):
- Container name field now always visible on every card (was hidden for
absent/not-installed containers)
- Connect button moved out of URL row into a dedicated dc-controls-row;
restyled as solid blue primary CTA
- Connect now toggles to Disconnect (green) when reachable, persists state
- On successful connect, URL is auto-applied to Settings so the backend
appears in TTS/STT dropdowns immediately — no separate "Use as" click needed
- "Use as TTS/STT" button updated: larger, teal "apply" style, tooltip,
gains .active highlight once applied, right-aligned as the trailing action
- Unified control order on every card: [Connect/Disconnect] [Stop|Start|Restart] [Use as →]
- initStaticDockerManagement rebuilt to use the same dc-controls-row layout
as dynamic cards, moving .llm-local-ping from URL row at init time
Conversation section:
- Fix chrome://flags URL being white-on-grey inside the mic-blocked warning
box (global code{background:var(--panel)} overrode the inherited white text)
- Inline-styled the <code> element with a dark translucent background
- Added a Copy button so users can copy the URL without selecting invisible text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- STT section now has a Quick test panel: select backend, hit mic button,
see transcript. Records via MediaRecorder, posts to /api/transcribe-bytes.
- _transcribe_audio detects the whisperx-gpu 'NoneType/to' error (caused by
pyannote/speaker-diarization-3.1 requiring a HuggingFace token) and
replaces it with an actionable message explaining how to fix it.
- _to_wav_16k added for STT audio conversion (Whisper/wav2vec2 expect 16kHz).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
STT models (Whisper, WhisperX VAD, wav2vec2 alignment) all expect 16kHz.
Sending 24kHz caused whisperx's VAD to miss speech segments, leaving
alignment with None inputs → 'NoneType has no attribute to' crash.
Added _to_wav_16k() and use it in conversation/turn and transcribe-bytes
endpoints. Health check probe also uses 16kHz silence for consistency.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port 8080 is Open WebUI — it passes /health + /v1/models checks but
returns 405 on POST /v1/audio/transcriptions. Updated probe logic to:
- treat 405 as 'endpoint missing, try next path'
- treat non-JSON 500 as broken, JSON-500 with detail as 'audio too short' (ok)
- use 500ms silence WAV instead of 1-frame (too tiny for alignment models)
Changed whisper.cpp default from :8080 to :8085 to avoid clash with
Open WebUI. Updated s-llms.html placeholder and code snippet accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_stt_backend_health now sends a minimal WAV to the transcription endpoint
after passing /health. A 500 response marks the backend unavailable,
catching containers that pass health checks but crash on model load (e.g.
CTranslate2 built without CUDA support).
Error messages from _transcribe_audio now include the backend URL and
replace generic 'Internal Server Error' with an actionable explanation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_transcribe_audio now extracts the response body on HTTP errors so the
actual cause (e.g. 'CTranslate2 not compiled with CUDA support') reaches
the user instead of '500 Server Error for url: ...'. Conversation panel
also strips the 'HTTP 500:' prefix to show only the meaningful part.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cardType() was defined inside initLlmsSection() IIFE but called from
renderLocalContainers() which is outside that scope, causing a silent
ReferenceError that reset every Connect click to failure. Moved cardType
to module scope.
Custom STT cards (e.g. whisperx-gpu) are now included in /api/stt-backends
and appear in the Conversation STT dropdown. Added _normalize_service_url()
so 0.0.0.0 URLs in stored cards are rewritten to host.docker.internal for
server-side health checks. _transcribe_audio() tries /transcribe as fallback
for custom backends that don't expose /v1/audio/transcriptions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs: (1) populateConvTtsBackends() referenced window._ttsBackends
which doesn't exist — fixed to use module-scope _ttsBackends directly.
(2) Broken hook that wrapped window.refreshTtsBackendAvailability (never
set on window) — replaced with _ttsRefreshHooks array dispatched at end
of refreshTtsBackendAvailability().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
probe-url now accepts a type param (llm/stt/tts) and checks service-
specific endpoints: LLM → /v1/models with data[] key, STT → /health
then /v1/models, TTS → /health then /voices endpoints. Random websites
and wrong services are now rejected. Connect passes the card's section
type; success toast shows which endpoint responded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the filter-to-available-only approach with full lists that
include unavailable backends (disabled, marked ✗) so users can see
what's broken. Also extends STT retry fallback to cover HTTP 500 from
wrong model names (fixes faster-whisper CTranslate2 CUDA build issue).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
navigator.mediaDevices is undefined on non-secure origins (HTTP + IP).
Add early guard with red banner in the chat panel, disabled mic button,
and an 8-second toast explaining the two fixes. Also add optional ms
parameter to toast().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>