604 KiB
Changelog
All notable changes to TTS Voice Creator — Clone and Design are documented here.
Follows Keep a Changelog · versioned with Semantic Versioning.
Unreleased
Fixed
- Per-model speaker embeddings stay with their voice: the TTS engine's
<name>.1.7b.pt/<name>.0.6b.ptfiles that sit beside the WAV are now part of the voice package, so hiding, unhiding, trashing, deleting and renaming a voice moves, removes or renames them too instead of leaving orphans inactive_voices. - Custom TTS engines no longer count as available just because the app's own voice library is non-empty: availability follows the engine's health, so a stopped VoiceDesign/CustomVoice container is no longer listed.
1.61.0 — 2026-10-11
Added
- My Voices list header: a sort header (select-all/none checkbox with a partial-selection state, then sortable Name / Lang / Gender / Speed / Rating with the active column highlighted) sits directly above the voice list, and the filter row starts with a filter icon. The full table header still takes over in table view.
- QA: Python tests in a throwaway container (
npm run qa -- --only pyunit --container): runs the unit tests on production's Python 3.11 in a no-network, read-only container built from the app image on a writable throwaway copy of the read-only mounted repo (docker/Dockerfile.testadds onlyhttpx2andnodejs); opt-in, SKIPPED when docker or the image is missing. - Sonar triage
--drain: works through open agent issues one at a time, fires the multica webhook, waits for the agent's answer on GitHub and resends once if nothing arrives; stops on a stalled issue. - Sonar triage can fire a multica webhook autopilot after each GitHub issue (URL from
~/.config/multica-webhookorMULTICA_WEBHOOK_URL, passed to curl on stdin only); the multica CLI stays as fallback.
Changed
- My Voices bottom bar simplified: Refresh, Sync and Import bundle stay as icon buttons (labels as tooltips / for screen readers) so the whole bar fits one row; every selection-based action now lives in one Actions (N) menu grouped Edit / Audio / Share / Danger (disabled until voices are checked; closes on pick, outside click or Esc).
- My Voices preview text / synth source ("Synth uses" Preview text / Reference transcript + sample sentence) moved from its own always-visible card into a drawer behind an icon in the list's top bar (state remembered; the icon tooltip shows the active mode). In the stacked/mobile layout the voice list now has a bounded height and the page scrolls, so opening both drawers no longer squeezes the rows or clips the bottom toolbar.
- My Voices filter area redesigned: one search row with a Filters button (badge = number of active filters) plus group-by-tag / table-view buttons. Language, gender, type, tag, rating, group, sort field/order, "Show disabled", "Clear filters" and the Groups accordion moved into a labelled, collapsible panel (remembered per browser); the sort header above the list stays on one line.
- My Voices list: the Groups chip list is a collapsed accordion (summary shows the group count and the active group, with "Show all"); the bulk actions (Set tag, Set source, Hide, Unhide, Rate, Delete) moved from the selection bar into the bottom toolbar; every selection-based button (Export selected, Copy selected, Precompute, Calc dB, Benchmark, Batch seeds, Verify, Set tag, Set source, Hide, Unhide, Rate, Delete) is disabled, with a tooltip, until voices are checked; only Refresh, Sync and Import bundle are always enabled; the redundant All / None buttons are gone (use the header checkbox).
- QA: the Sonar triage script groups medium/large findings per rule instead of per (rule, file): one GitHub issue per rule with a file/line table (max 40 rows, 'and N more'; split into 'part n/m' beyond 40 locations); dedupe marker still carries all Sonar keys.
- Conversation Playground: the round microphone button is red (pressed, "Stop microphone") while the mic is hot (live or recording) and blue ("Start microphone") when off; it returns to blue on stop, auto-stop and mic errors. The pulse ring only animates when reduced motion is not requested.
- Conversation Playground: the metrics (Token Usage and STT / LLM first token / TTS first audio TTFB charts) now sit in the right-hand Latency column below Turn history instead of a second "Metrics" tab; the Conversation | Metrics tablist is removed. Charts gain axis titles ("Time (ms)", "Time"), a legend and three y-ticks.
Fixed
- Status-bar TTS picker did not drive My Voices "Play": choosing an engine in the status bar updated the preview/reader/rehearser selectors but not the My Voices (library) one, so Play kept using the previously selected engine (e.g. a stopped VoiceClone) and failed with a 500. It now also sets the library and Speech-to-Text-test selectors, and the toast names the engine.
- Unhidden voices fell back to the default voice: unhiding moved the files but never registered the voice in the TTS engine's
voices.json(only saving a transcript or importing did), so the engine logged "Voice … not configured; falling back to default voice" and spoke with another voice. Unhiding now publishes a missing entry from the voice's reference transcript; existing entries (and their cached speaker embeddings) are left alone. - "No TTS backend" although engines show Connected: the TTS health probe appended
/healthto the card URL (…:4000/v1/health= 404), so every custom TTS engine behind a LiteLLM gateway was judged unreachable. Card URLs ending in/v1are now probed from the server root, and for engines behind a LiteLLM gateway the probe asks the gateway (/model/info, cached 30 s) for the selected model's upstream and checks that engine itself, so a stopped VoiceClone container is shown as unavailable even though the gateway is up (the gateway's unauthenticated/health/livelinessis only the fallback); and "Use as TTS / STT" refreshes the status bar immediately. - Copy buttons did nothing (My Voices "Copy selected", Connect "Copy active voices", engine/voice-inspector/voice-source copy):
copyTextwas no longer exported from utils.js after the module scoping, so the click threwReferenceError. It is exported again, now reports whether the copy succeeded, and "Copy selected" shows a copy icon, a "N voice names copied to clipboard" message, and a dialog with the pre-selected list when the async clipboard API is unavailable or refused (plain-HTTP LAN origin, embedded webviews), since the legacy fallback cannot be verified. - Sonar: removed 43 small Sonar findings in 20 files: unused variables and dead helper functions (frontend), unused locals and a misread comment (backend); behaviour unchanged. 7 further small findings were deliberately left (used via globals, or entangled with other dead code).
- Conversation Playground: Turn history rows (#n, time, error rows) were in the DOM but squeezed to zero height by the flex column once charts filled the Latency column; the list no longer shrinks.
- Conversation Playground: the metrics charts render at the column width (re-rendered on resize) so axis, tick and legend text is 11px instead of ~8px; a single point is centred with a larger dot.
Added
- QA:
scripts/sonar_triage.mjs(npm run sonar:triage), a local SonarQube triage loop (dry-run default): classifies findings small/medium/large/skipped via allow/skip lists, files one GitHub issue per rule (file/line table, max 40 locations, larger rules split into 'part n/m') with a dedupe marker, hands them to the multicasonar-fixeragent, and can auto-fix small ones as a local commit. Seearchitecture/sonar-triage.md. - Conversation Playground: Metrics view (Conversation | Metrics tabs) with Pipecat-style TTFB line charts for STT, LLM first token and TTS, plus session Token Usage (prompt/completion/total, "n/a" when the backend reports none). The stream now requests
stream_options.include_usage(dropped and retried on 400/422) and forwardsusagein the final stats event; the TTS value is now the time to the first audio chunk instead of including the remaining LLM generation time. - Conversation Playground: a Thinking on/off switch next to the Language Model (default off, setting
conversation_thinking). Off sendschat_template_kwargs.enable_thinking=false(plusthink:falsefor Ollama) with each reply request and never touches the system prompt; the empty-reply error now says to turn Thinking off.
Fixed
-
Async routes no longer block the event loop on synchronous HTTP: seed proxy/samples, text refine/persona rewrite, voice matching (its retry back-off is now
asyncio.sleep), character deep analysis, character image-from-URL, LLM/engine model listing, source audio import/proxy and ElevenLabs shared voices now runrequeststhroughasyncio.to_thread(same timeouts and responses). -
Silently swallowed errors now surface: failed saves of the audiobook prompt default, generated character sheets and character colours show an error toast, and ~20 other hidden failures (rehearsal/cast write-back, reader progress and audio fetches, seed-finder DB writes, settings/model fetches, export server save) are logged with
logErrinstead of being dropped.
Changed
- Conversation Playground: the Language Model base URL is now picked from a dropdown of the language models configured under Engines (Docker LLM containers incl. saved URL overrides, and custom LLM cards; running/stopped marked), with a "Custom…" option that reveals the free-text URL. Selecting an entry saves
conv_llm_urland fetches the models. - Playwright specs now import
testfromtests/_guard.js, whose auto fixture aborts and fails any unmocked non-GET request to/api/**so a spec can no longer modify live settings. Seearchitecture/qa.md.
1.60.0 — 2026-10-04
Added
npm run qa: central QA command (scripts/qa.mjs) with timed stages (lint, python/node unit tests, Playwright, security scans, bundle freshness) and an opt-in SonarQube stage (--sonar, token from the environment only). Seearchitecture/qa.md.
Changed
npm run qa: pyunit gets a fresh temp HOME/CONFIG_DIR/TMPDIR per run (cleaned afterwards);--sonarstage also produces Pythoncoverage.xmlwhencoverageis installed (else SKIPPED).guided_installer.py: command execution extracted into a testablerun_commandhelper (lowers cognitive complexity, python:S3776); no behaviour change.- Voice list rows no longer show the "Source unknown" badge (it crowded the play buttons). The badge stays in the voice inspector, next to the Source & consent panel.
Fixed
- Magazine specs no longer touch the live settings. The export-provenance checkboxes read and saved
/api/settingson the real app (a run left the default off, failing the next one); the spec now mocks it. The hostile deep-link spec waits for the lazily loaded magazine module before calling the parser. - Duplicate German i18n keys and dead frontend code. Removed 14 shadowed duplicate keys in
i18n.js(effective translations unchanged), the unreachable body ofaudiobookShowPreview, and two identical-branch ternaries. - Security hardening (Sonar findings). The Docker installer downloads
get-docker.shinto a privatemkdtempdir (not a predictable/tmppath) and removes it afterwards; the OMDb poster lookup uses HTTPS; the fish.audio cache key marks MD5 asusedforsecurity=False. - Background jobs could be garbage-collected mid-run. Fire-and-forget
asyncio.create_taskcalls (magazine, conversation, tts, stt, library, sources) now go throughcore.tasks.spawn, which keeps a strong reference until the task ends and logs failures by exception class only. - Voice archetype gallery broke
npm run check:frontend: itssayhelper collided withmagazine.js; renamed toarchSay. - Export failed with HTTP 500 when provenance tagging failed (EPUB/ZIP/article MP3 with audio ffmpeg cannot tag); it now ships the untagged file and logs a warning.
- Archetype
languagefilter accepted only full names.GET /api/archetypes?language=dereturned 0 items; it now accepts ISO codes and names, case-insensitive (de,DE,German); unknown values return an empty list. - LibriVox search returned 502 "unavailable" after 20-30 s. Cause: the LibriVox API is erratic (10-40 s for any request, measured from host and container; egress/DNS/TLS fine) and the old call asked for
extended=1(all chapters of 15 books) with a 30 s timeout, mapping every failure to one generic message. Now the list is light (noextended), the first chapter is fetched per book on demand (GET /api/voice-sources/librivox/book/{id}, UI "Load preview" / on import), timeout 25 s with one retry and backoff, failures cached 30 s, distinct messages (timeout / unreachable / HTTP status / unreadable) and a privacy-safe log line (exception class and query length only). Host/redirect rules unchanged. export_provenance_defaultmissing from settings defaults. It is now in the defaults (true), soGET /api/settingsexposes it.
Added
- Voice archetype gallery (own, German-first). "Voice gallery" card in Design a Voice: 40 hand-authored named voices (mostly German, a few English) in 7 categories, each a free-text Qwen3 VoiceDesign description with facets (gender, age, pitch, accent, tone) from the design-chip taxonomy. Search, facet chips, category list, favourites (browser storage), 24 cards per page rendered only after opening, labelled Preview (aria-live status) and "Use voice" (then "Open in Design" prefills the editor).
GET /api/archetypes,POST /api/archetypes/{id}/preview(cached by sha256 of instruct, language, engine and seed 42; timeouts, blank-audio guard) andPOST /api/archetypes/{id}/use(saves voicearchetype_<id>, origin designed, provenance basisunknown, sourcearchetype/<id>, note "synthetic designed voice"; idempotent). Own catalog, nothing copied from other projects; no network. Optionalscripts/render_archetype_previews.pypre-renders all previews. Tests:tests/test_voice_archetypes.py,tests/archetype-gallery.spec.js. Seearchitecture/voice-archetypes.md. - Audiobook emotion tests. Playwright
tests/audiobook-emotion.spec.js(mocked routes) covers span-cast emotion ->instructin/api/tts-preview(dialogue only, English tag, narration empty) and the line cache key;tests/test_tts_instruct_payload.pycovers the instruct passthrough. - LibriVox and Wikimedia Commons voice sources. New tab in Get Voices Online searches the official LibriVox feed and the Commons MediaWiki API through
GET /api/voice-sources/{librivox|wikimedia}?q=&lang=(fixed hosts, query cap 80, 10 results, 30 s timeout, 10 min cache, 1 s throttle, descriptive User-Agent, result URLs host-checked). Results show licence and a preview; "Import clip" trims (start/length) and stores provenance (LibriVox: public domain + check-the-reader note; Wikimedia: licence + attribution).archive.organdupload.wikimedia.orgadded to the audio proxy allowlist. Tests:tests/test_voice_sources_new.py,tests/voice-sources-new.spec.js. Seearchitecture/voice-sources.md. - Provenance tags on exported audio. Magazine MP3 downloads,
audio.zipand the MP3s inside the EPUB get ID3 tags (synthetic_speech=true,generator,generator_version,generated_date, comment; voice name only on opt-in) via one ffmpeg stream copy of a temp copy; stored audio is untouched. Per-export checkboxes on the magazine export row ("Mark as synthetic speech", "include voice name"), default from new settingexport_provenance_default(on).POST /api/export/provenanceandpython -m core.export_provenance file.mp3read the tags back. Inaudible AudioSeal watermarking is researched (MIT code and weights, torch needed, weak message recovery after MP3/AAC) but only a stubwatermark(path)that reports unavailable. Spoken disclosure is not implemented yet. Tests:tests/test_watermark_tags.py,tests/magazine.spec.js. Seearchitecture/watermarking.md. - Inline text tags for TTS (setting
inline_tags_enabled, default OFF).[pause 500ms]/[pause 1.2s](max 10 s each, 60 s per text),[spell]USA[/spell](German letter names for German text),[slow]..[/slow]/[fast]..[/fast](0.85 / 1.15 via ffmpeg atempo, innermost wins, unclosed ends at paragraph). Known control tags (e.g.[laugh]) are stripped, never spoken;[1]and[sic]stay. Rendered inPOST /api/tts-preview(also used by audiobook line previews),/v1/audio/speech(non-streaming),/speakand the MCP speak tool through one sharedssml_lite.render_with_tags; stripped only (never spoken) in streaming, spoken chat replies and magazine MP3 (timings stay exact). Settings -> General switch (accessible, DE/EN) and a syntax hint under the Try It Out and Read Aloud text boxes. Tests:tests/test_ssml_lite.py,tests/test_inline_tags_paths.py,tests/inline-tags.spec.js. - Optional vocal isolation for URL/YouTube imports (default OFF).
POST /api/isolate-vocals {id}runs the Demucs CLI (htdemucs, two stems) on a registered temp audio and registers the vocals as a new id; the original stays valid. NeedsVOCAL_ISOLATION=1, ademucsbinary (orVOCAL_ISOLATION_CMD) and the weights already on disk; the app never downloads anything and nothing is added to the main image. Limits: 15 min input, 15 min runtime (VOCAL_ISOLATION_MAX_SECONDS/_TIMEOUT), one job at a time, cancelled on client disconnect, temp files always removed.GET /api/versionreportsfeatures.vocal_isolation; the "Remove background music" button (clone page URL card and voice library add-URL) appears only then, with an aria-live status and a play original / play without music toggle. Tests:tests/test_vocal_isolation.py,tests/vocal-isolation.spec.js. Seearchitecture/vocal-isolation.md. - Voice export/import bundles.
GET /api/voices/{id}/exportstreams a<id>.voice.zip(audio,reference.txt,meta.jsonwith seed, design prompt, rating/tags,provenance,audio_problems; optional picture and consent recording;manifest.jsonwith format version, app version and sha256 per file; no temp files).POST /api/voices/importvalidates at the boundary: 150 MB upload cap, fixed flat member names only (zip-slip/path traversal rejected), per-file, total and compression-ratio caps, hashes verified, audio decoded, meta and provenance re-validated; a taken name gets a_2suffix, an identical voice is a no-op, nothing is overwritten. Voice library toolbar: "Export selected" and "Import bundle" (44px). Tests:tests/test_voice_bundle.py,tests/voice-bundle.spec.js. Seearchitecture/voice-bundle.md. - Consent/provenance field per voice. Each voice gets
provenancein its.meta.json(basis: own_voice, consented, licensed, public_domain or unknown;source,licence,note, optionalrecorded_consent,set_at). Existing voices count asunknown;quick-import-voiceprefills the source URL (basis staysunknown). NewPUT /api/voices/{id}/provenance(enum, length caps, control characters stripped, http(s) URLs only, no credentials) andPOST/GET /api/voices/{id}/consent-recording(audio only, 10 MB cap, stored as<voice>.consent, moves/deletes with the voice). The voice inspector has a "Source & consent" section (typed note or MediaRecorder recording) and voices without a recorded basis show a "Source unknown" badge. Generation is not blocked. Data stays local. Tests:tests/test_voice_provenance.py,tests/voice-provenance.spec.js. - Seed Finder: Auto-pick best.
POST /api/seed-autopick {voice, text, seeds|count}(default 8, max 16) generates candidates sequentially through the existing temporary-voice path (voices.json is always restored), scores each 0-100 (ASR word accuracy, loudness/clipping,audio_problems) and returns all sub-scores plus the winner. Degrades to loudness/problems when STT is unavailable. The UI highlights the winner; saving stays a click on "Use seed". Tests:tests/test_seed_autopick.py,tests/seed-autopick.spec.js. - Voice Design: attribute chips. Toggle chips (gender, age, pitch, pace, accent/region, tone/timbre, emotion; German + English labels) above the design prompt add or remove only their own phrase in the free-text description. The textarea stays fully free-form. Test:
tests/design-chips.spec.js. - Voice health: timestamped audio problems.
core.audio.audio_problemsflags long interior silence, clipping and sudden level jumps (problemslist withkind,start_s,end_s,detail, plus one warning per kind) in the route-test voice health; existing keys unchanged. Computed on demand and cached by file mtime, not in the voice list. Shown in the routing test result and in the Try-out voice panel (newGET /api/voices/{id}/health); imports store the result in meta (audio_problems). Tests:tests/test_audio_problems.py,tests/voice-health.spec.js.
Docs
- Audiobook emotions section in
Manual.md(filling, editing, voice types that honour them, limits) and an Emotions section inarchitecture/audiobook-attribution.md.
Changed
- Magazine interviews: journalist/guest voices. Interview labels such as
c't:(alsoc’t,ct, OCRCc't),Redaktion,Interviewer,Frageare the interviewing side and are read in the narrator voice; every other recurring label is a guest and gets the second voice. Unlabelled blocks continue the current speaker's turn; headings, decks and bylines stay narrator. Detection stays strict (label starts a block and recurs at least twice). Audio-timing records now store each segment's voice and speaker. Tests:tests/test_magazine.py(InterviewJournalistTests).
1.59.0 — 2026-10-04
Changed
- Characters: global cast view in the sidebar, per-book characters inside Studio. Library > Characters / Cast is now titled "All characters (all books)": one collapsible band per book (heading, per-book count,
aria-expanded), first book open and the rest closed and not rendered until opened (DOM budget), and a Search all books field. Studio > Voices lists only the book picked in Source, under a "Book: