7f8a42ce51387cbe43744021f02dc85a5d4c0ba1
759
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7f8a42ce51 |
fix(tts): mlx-audio CSM cloning drops ref_text, breaking every clone attempt (#1012, #1013) (#1017)
MLXAudioBackend.generate() reads voice/ref_audio/language/speed from its kwargs but never extracted ref_text — it was built, then silently never passed through to self._model.generate(). CSM (sesame.py) only builds its cloning context when BOTH ref_audio AND ref_text are present; with ref_text missing, the context list stays empty and indexing into it raises "IndexError: list index out of range" deep inside mlx-audio, instead of the clone ever being attempted. Voice cloning on the CSM engine could never have worked as shipped. generation.py already threads ref_text all the way through — even auto-transcribing it via the GPU pool when the caller supplies ref_audio without one (~line 780) — so the value was always available in kwargs; it just never survived the crossing into this specific backend. Reported with the precise root cause and a working fix (community member independently diagnosed and patched it locally, confirmed working on MPS/0.3.12). Two-line fix: extract ref_text and pass it through when both ref_audio and ref_text are present (guards against passing an orphaned ref_text with no accompanying audio to engines that don't expect it). Tests: tests/test_engines.py — ref_text is passed through when paired with ref_audio, omitted when ref_audio is absent. Also documents the second bug from the same report (#1013): macOS microphone permission never prompts, so OmniVoice never appears in System Settings to grant access. Root-caused to an unresolved upstream Tauri/WebKit limitation (WKWebView's requestMediaCapturePermissionFor delegate — wry#1195, tauri#11951, fix wry#1196 still open/unmerged, no released version to bump to) — not something fixable here without an unverified native Rust/WKWebView hack this session has no way to test. Documented in docs/install/troubleshooting.md with the confirmed workaround (record elsewhere, upload the file). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4de3c824d0 |
fix(gallery): surface the real error instead of a generic guess (#1009)
Found while triaging Discord: a community member (lehoangan227) hit
"Could not create that voice — the engine may be loading" trying to
use an archetype from the Gallery. That message is hardcoded and
shown for ANY failure — the actual cause (a 500, a validation error,
anything) is caught and discarded.
api/client.js's ApiError already builds a clean, user-facing message
for every failure mode (HTTP status + backend detail, a network
failure, or a detected backend crash) — this codebase's own
established convention elsewhere is to interpolate that message via
`{{message}}` (see BatchQueue.jsx, Settings.jsx, ToolsPage.jsx). The
Gallery's own catch blocks just weren't following it.
Fixed the whole class across VoiceGallery.jsx (use/preview),
CommunityZone.jsx (add-to-voices, whose catch clause didn't even bind
the error), and ImportsZone.jsx (search/upload/save/delete/trim —
handleDelete previously failed completely silently, no message at
all). All now interpolate the real error message, matching the
gallery.download_failed key that already did this correctly a few
lines away.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
11fbbd3f6d |
fix(dub): cross-language dub no longer speaks source-language reference text verbatim (#1004) (#1008)
extract_speaker_clones/extract_segment_refs pair each audio slice (cut at ASR segment timestamps) with that segment's own `text` field, on the assumption the two agree. They routinely don't — Whisper (and friends) frequently drift on segment boundaries: a trailing word audible in [start, end] but missing from text, or vice versa. When the (ref_audio, ref_text) pair disagrees, zero-shot TTS prompt-priming breaks down and the clone can emit the mismatched reference text itself instead of the target-language line it was asked to speak — reported with an exceptionally clear root-cause diagnosis and a working A/B repro (matched pair: clean on the first try; mismatched pair: wrong language 6/6 times). Fix (as proposed in the report): re-transcribe each written reference clip via the already-loaded, already-warm active ASR backend and use that transcript as ref_text — this guarantees the pair matches by construction, independent of whether the original segment text was ever right. Falls back to the original text on any re-transcribe failure or empty result — never a regression from current behavior, only ever a fix. New services.speaker_clone.refine_ref_text (single clip, unit-testable against a duck-typed fake ASR backend) and refine_ref_texts (batch — one executor round-trip per whole clones/seg_clones dict rather than one per reference). Wired into dub_core.py's two clone-extraction call sites, routed through _gpu_pool to match the established convention for ASR-backend calls (the model is mid-lifecycle: TTS is offloaded, ASR is loaded and exclusive, right where the existing per-chunk transcribe calls already run on this same pool). Tests: tests/test_speaker_clone_purity.py — 6 new cases covering the mismatch-correction path, ASR-failure fallback, empty-transcript fallback, no-backend no-op, and batch behavior (one failing entry doesn't affect the others). Full backend suite: 2412 passed, 0 failed. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f546f04c8e |
fix(ci): bump Linux release runner to ubuntu-24.04, fixing stale bundled WebKitGTK (#961) (#1007)
The AppImage bundles whatever libwebkit2gtk-4.1-dev the build runner's apt repos resolve at build time (see the "Linux system deps" step) — AppRun's LD_LIBRARY_PATH then makes that bundled copy take priority over the host's system WebKitGTK at runtime. ubuntu-22.04's version was stale relative to what current distros (Ubuntu 24.04+, Fedora 44) ship, which is why a from-source build (linking straight against the host's healthy system library) worked fine on the exact machine where the shipped AppImage white-screened — the released binary was running an older, buggier WebKitGTK under the hood regardless of the host. Bumped the Linux release matrix entry to ubuntu-24.04, and ci.yml's Tauri shell-check job to match (its own comment already says "Mirror release.yml" — now it actually does, so a green PR check accurately predicts the release build will also succeed). Raises the AppImage's glibc floor from 2.35 to 2.39 (Ubuntu 24.04+) — README's system-requirements table corrected from the now-false "Ubuntu 20.04+" claim. No reports of anyone on a pre-2022 distro. This does not fix the AppRun launcher's separate, related bug (its WebKitGTK-version auto-detection reads the *system's* pkg-config version, not the version actually bundled and running) — that would need a reliable way to read the bundled .so's version from within the AppImage, which isn't straightforward (WebKitGTK's soname doesn't map 1:1 to its release version) and isn't verifiable without a real Linux build environment to test against. Left as a known, separate gap. Cannot be verified from here on a real Ubuntu 26.04 machine — shipped on the strength of the root-cause diagnosis, pending the reporter's confirmation. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
270b3b1c4c |
fix(backend): quitting mid-preload no longer reports a clean shutdown while a GPU-pool thread is still importing (#1000) (#1002)
A user-pasted backend log revealed the real cause behind a class of 'can't reach backend' reports: three rapid restart cycles, each ending with 'Shutdown: done.' immediately followed by a 'Model loading failed: Could not import module AutoFeatureExtractor' error. That error text is transformers' own generic lazy-import wrapper (import_utils.py's _LazyModule.__getattr__), not a real dependency problem — pyproject.toml already pins transformers/torch/torchaudio/soundfile/librosa as core, non-optional deps, and the same venv loaded the model successfully 90 seconds later in the same log. Root cause: preload_task and capture_preload_task were created at startup but never referenced in the lifespan shutdown block — idle_task and worker_task got cancelled-and-awaited, the preload tasks were simply abandoned. Cancelling an asyncio task awaiting run_in_executor() can't stop the underlying OS thread once it's inside blocking import/load work, so 'Shutdown: done.' logged while a GPU-pool thread was still mid-Version: ImageMagick 7.1.2-25 Q16-HDRI aarch64 037e46295:20260604 https://imagemagick.org Copyright: (C) 1999 ImageMagick Studio LLC License: https://imagemagick.org/license/ Features: Cipher DPC HDRI Modules Delegates (built-in): bzlib freetype heic jng jpeg lcms ltdl lzma png tiff webp xml zlib zstd Compiler: clang (21.0.0) Usage: import [options ...] [ file ] Image Settings: -adjoin join images into a single multi-image file -border include window border in the output image -channel type apply option to select image channels -colorspace type alternate image colorspace -comment string annotate image with comment -compress type type of pixel compression when writing the image -define format:option define one or more image format options -density geometry horizontal and vertical density of the image -depth value image depth -descend obtain image by descending window hierarchy -display server X server to contact -dispose method layer disposal method -dither method apply error diffusion to image -delay value display the next image after pausing -encipher filename convert plain pixels to cipher pixels -endian type endianness (MSB or LSB) of the image -encoding type text encoding type -filter type use this filter when resizing an image -format "string" output formatted image characteristics -frame include window manager frame -gravity direction which direction to gravitate towards -identify identify the format and characteristics of the image -interlace type None, Line, Plane, or Partition -interpolate method pixel color interpolation method -label string assign a label to an image -limit type value Area, Disk, Map, or Memory resource limit -monitor monitor progress -page geometry size and location of an image canvas -pause seconds seconds delay between snapshots -pointsize value font point size -quality value JPEG/MIFF/PNG compression level -quiet suppress all warning messages -regard-warnings pay attention to warning messages -repage geometry size and location of an image canvas -respect-parentheses settings remain in effect until parenthesis boundary -sampling-factor geometry horizontal and vertical sampling factor -scene value image scene number -screen select image from root window -seed value seed a new sequence of pseudo-random numbers -set property value set an image property -silent operate silently, i.e. don't ring any bells -snaps value number of screen snapshots -support factor resize support: > 1.0 is blurry, < 1.0 is sharp -synchronize synchronize image to storage device -taint declare the image as modified -transparent-color color transparent color -treedepth value color tree depth -verbose print detailed information about the image -virtual-pixel method Constant, Edge, Mirror, or Tile -window id select window with this id or name root selects whole screen Image Operators: -annotate geometry text annotate the image with text -colors value preferred number of colors in the image -crop geometry preferred size and location of the cropped image -encipher filename convert plain pixels to cipher pixels -extent geometry set the image size -geometry geometry preferred size or location of the image -help print program options -monochrome transform image to black and white -negate replace every pixel with its complementary color -quantize colorspace reduce colors in this colorspace -resize geometry resize the image -rotate degrees apply Paeth rotation to the image -strip strip image of all profiles and comments -thumbnail geometry create a thumbnail of the image -transparent color make this color transparent within the image -trim trim image edges -type type image type Miscellaneous Options: -debug events display copious debugging information -help print program options -list type print a list of supported option arguments -log format format of debugging information -version print version information By default, 'file' is written in the MIFF image format. To specify a particular image format, precede the filename with an image format name and a colon (i.e. ps:image) or specify the image type as the filename suffix (i.e. image.ps). Specify 'file' as '-' for standard input or output., and interpreter finalization tore down module state under it — producing exactly this misleading error. Fix: extract the existing cancel+bounded-await pattern into _cancel_and_await_tasks() and apply it to all four background tasks, not just two. An early-stage load (still importing, not yet mid weight- download) now gets a real chance to finish before shutdown proceeds; a load genuinely deep in blocking work still times out at the same 3s bound, and _reset_gpu_pool() abandons it same as before. Also: both error handlers around this path logged only str(exc), discarding __cause__ — added exc_info so a future incident (even one this fix doesn't fully prevent) surfaces the real underlying error instead of the misleading generic wrapper text. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5a7d9cc05c |
feat(asr): generic OpenAI-compatible transcription backend (#877) (#1003)
First slice of the community's two-track proposal for #877: a generic OpenAI-compatible ASR backend that works TODAY, without waiting on transformers to ship a direct Qwen3-ASR integration (tracked separately, still blocked upstream). Points OmniVoice's transcription at any server exposing POST /v1/audio/transcriptions — a self-hosted Qwen3-ASR/ FunASR/SenseVoice server, or OpenAI's own API. - New OpenAICompatASRBackend (backend/services/asr_backend.py): a pure network client, no local model, no install. Prefers response_format=verbose_json for real per-segment timestamps, degrades to plain text (matching MoonshineASRBackend's shape) when a minimal server rejects that format. Never leaks a raw SDK/httpx exception to the caller (#977 convention) — wraps network/auth failures in a clean, actionable RuntimeError naming the server. - Settings persist via the same encrypted-secret convention as services/llm_providers.py (settings_store.set_secret for the API key — Fernet-encrypted, never a .env row, never echoed back; get_text/ set_text for base_url/model). New GET/PUT /api/settings/ asr-openai-compat, loopback-gated like every other settings route. - Frontend: a small settings panel (Settings → Models) mirroring HFMirrorPanel's exact structure. No ASR engine picker exists yet for ANY ASR backend (only TTS has one) — activating this engine still needs OMNIVOICE_ASR_BACKEND=openai-compat-asr; documented plainly rather than pretending otherwise. - README's ASR Engines table (9 → 10 engines) and docs/features.yaml's drift-checker inventory updated; the '9 engines, all fully local' claim corrected since this one genuinely isn't. - docs/engines/openai-compatible-asr.md: setup steps + an explicit privacy note (unlike every other ASR engine, audio leaves the machine to whatever server is configured). Regression tests: tests/test_asr_openai_compat_877.py (12 tests) — is_available() gating, verbose_json + plain-text response adaptation, network-failure error hygiene, SDK retry disabling, and the settings endpoints' persist/mask/clear-vs-unchanged semantics. Fixed two real full-suite-only failures found during verification (not brushed aside): the API route inventory snapshot needed regenerating for the two new routes, and this file's own tests had a module- staleness bug — a collection-time settings_store import went stale relative to a test-time-fresh fixture when another test elsewhere in the ~2400-test suite reimports the module — fixed by making settings_store itself a fixture resolved at test-run time, same lesson already applied to tests/test_mm2_lifecycle.py earlier this session. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
5871ec9d28 |
release: freeze v0.3.12 — version bump, lockfiles, changelog (#1001)
19-issue GitHub sweep: 16 PRs merged since v0.3.11, most fixing reports filed same-day or in the days prior. Highlights: MLX-Audio's 6 other curated models are finally selectable (was silently stuck on Kokoro regardless of what was downloaded), first-run no longer dead-ends behind restricted networks or corporate TLS-inspecting proxies, dubbing/batch TTS honor your active engine selection, and a run of sharp community diagnoses (ROCm wheel index, Windows dictation focus-steal, a genuine frontend crash regression) got fixed largely because reporters did the hard diagnostic work themselves. Full backend suite: 2390 passed, 0 failed. Full frontend suite: 918 passed, 0 failed. Version lockstep (tests/test_app_version.py): 6/6 passed. Docker frozen-lockfile parity (bun install --frozen-lockfile): clean, no drift. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>v0.3.12 |
||
|
|
58b023ff16 |
fix(diagnostics): TTS generate timeout message tells you to Flush/Unload (#939) (#999)
The guard itself (#851/#896) is working as designed — this is a message gap, not a logic bug. The GPU-branch timeout guidance explained VRAM contention but never mentioned the Flush/Unload action that actually resolves it, even though: (a) that action already exists (POST /system/flush-memory, wired to the header's Flush button), and (b) the sibling ASR-timeout guard's message already recommends it verbatim (asr_backend.py's _CUDA_VRAM_BUDGET_GB guidance). The maintainer ended up manually explaining 'Settings → Models → Flush caches / Unload' in an issue thread reply — information the error message should have carried itself. String-only change, no control-flow touched, mirrors the exact precedent of #896 (a guidance-only change to this same function). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b61d17dd61 |
fix(diagnostics): filter Tauri's benign IPC-fallback warning from frontend log capture (#975) (#998)
On some Windows configurations, Tauri's custom-protocol IPC probe fails once at startup and Tauri logs a console.warn before silently — and successfully — falling back to postMessage + WebSocket. Fully functional, happens at most once per launch, and not a bug in our code (confirmed: this is Tauri's own internal fallback mechanism, structurally intentional across its recent 2.11.x releases, not something being actively patched upstream — so not bumping the framework speculatively for this). It IS real noise though: as a captured console.warn it spuriously flips the Settings > Logs footer's Frontend pill to "1 warning" on every affected Windows launch. Filtered at the capture source (consoleBuffer.js) rather than the display layer, so it never enters the ring buffer or a copied diagnostic dump either — narrowly scoped to this one known message prefix, not a general warning-suppression mechanism. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
93aab6dadc |
docs(linux): mention yt-dlp as an optional prerequisite (#973) (#997)
The preflight system check already warns in-app when yt-dlp is missing (Voice Gallery/Dub YouTube downloads fail without it), but the install docs never mentioned it — a user has to hit the in-app warning first instead of seeing it up front alongside the other optional prereqs. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7098be1c8b |
fix(dictation): Windows pill no longer steals foreground focus (#982) (#996)
Root-caused and verified against the actual code (community diagnosis in #982 was accurate): showing the dictation pill — an always-on-top top-level WebviewWindow — grants it Win32 foreground activation by default on Windows, which macOS doesn't do for a shown window. Nothing marked the pill non-activating, so on Windows the synthesized Ctrl+V from `simulate_paste` landed back in the pill instead of the app being dictated into. Code review also found a second, more direct culprit at the same call sites: the global-shortcut Pressed handler's `win.set_focus()` was only skipped on macOS (`#[cfg(not(target_os = "macos"))]`), so Windows explicitly focused the pill on top of the implicit Win32 activation; the tray "dictate" menu handler called `win.show(); win.set_focus();` unconditionally on every platform, including Windows. This is the Windows counterpart of #287 (macOS auto-paste — don't steal focus): same class of bug, same "pill must stay unfocused so the paste lands in the target app" intent (already stated in the `grant_webview_ media_permissions` module comment), different OS mechanism. Fix, mirroring #287's platform-cfg pattern: 1. WS_EX_NOACTIVATE applied to the pill's HWND once, right after creation (`mark_pill_noactivate`), via the `windows` crate pinned to the exact 0.61.x tauri itself already resolves to — Cargo.lock confirms this unifies to the same `windows 0.61.3` already in the graph, so `WebviewWindow::hwnd()`'s HWND and our Win32 calls share one type and no new crate version was added. 2. `ShowWindow(SW_SHOWNOACTIVATE)` (`show_pill_noactivate`) in place of `.show()` at the two dictation-trigger call sites (global shortcut + tray "Start Dictation"), since `.show()` alone still raced the style bit on some paths. 3. The explicit `set_focus()` calls at those same two sites are now skipped on Windows too, the same way they already were on macOS. macOS and Linux are untouched: the macOS cfg branch is unchanged, and the Linux branch of the `set_focus()` guard still runs exactly as before. The pill's auto-dismiss (`scheduleDismiss`/`dismiss` in CaptureWidget.jsx) was checked and is a plain unconditional setTimeout chain — it is not gated on any native focus-loss/paste-completion signal, so there's no independent bug to fix there. The "never dismisses" symptom is a consequence of the focus-steal, not a separate stall: once the pill wrongly held foreground for the whole session, hiding it later left Windows' foreground state inconsistent. With the pill never taking focus, the target app stays foreground throughout and there's nothing to reconcile. Win32 window-activation syscalls (`#[cfg(target_os = "windows")]`) can't run under `cargo test`/`cargo build` on this non-Windows CI runner, so the new `pill_noactivate_tests` module tests the pure flag math (`with_noactivate_style`) instead — platform-agnostic, runs everywhere, verified passing here. The actual HWND-touching code is logic-reviewed but UNVERIFIED on real Windows; the reporter offered to test a patched build, which is the recommended next step before this ships in a release. `cargo build` and `cargo test --lib` both pass (60/60 tests, including the 3 new ones); the pre-existing `setup.rs` unreachable_code warning (#286) is unrelated. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
8520b84b68 |
fix(clone): voice-design panel no longer crashes on a partial vd_states shape (#983) (#995)
Crash: DesignMethodPanel's optLabel() called val.replace(...) on an
undefined category value — a regression from
|
||
|
|
549fa4009f |
feat(engines): expose MLX-Audio's curated model picker (#981) (#994)
mlx-audio multiplexes 7+ curated models (Kokoro, CSM, Qwen3-TTS, Dia,
Chatterbox, MeloTTS, OuteTTS) behind a single "mlx-audio" backend id, but
MLXAudioBackend resolved its active model ONLY from the
OMNIVOICE_MLX_AUDIO_MODEL env var — invisible to Settings and unreachable
without restarting the packaged app with that var set. A user who
downloaded e.g. Llama-OuteTTS via Settings → Models had no way anywhere
in the UI or API to actually load it; the backend silently kept using
Kokoro.
Fix:
- MLXAudioBackend.__init__ now resolves its model via
prefs.resolve("mlx_audio_model_id", env=..., default=...), mirroring
active_backend_id()'s env > prefs > default order exactly.
- get_active_tts_backend()'s switch-detection now also tracks the
resolved mlx-audio model key, so a model-only change (same backend id)
invalidates the cached instance and reconstructs it — no app restart
needed to pick up a different curated model.
- POST /engines/select gained an optional model_id field; for
family=tts/backend_id=mlx-audio it validates against
MLXAudioBackend.CURATED_MODELS (or a raw HF repo id, matching the
class's existing tolerance) and persists it via prefs.
- GET /engines now includes a curated_models roster + active_model_id on
the mlx-audio entry only.
- Settings → Engines renders a small model dropdown on the mlx-audio row,
pre-selected to the active model, wired through selectEngine's new
optional modelId argument.
Regression coverage: prefs resolution + env override, cache invalidation
on model-only switch, /engines/select 400s on an unknown model id and
persists a valid one, curated_models present only on mlx-audio, and a
new EngineCompatibilityMatrix vitest suite for the dropdown.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
dfe2bd1bc3 |
fix(install): classify SSL handshake failures + trust the OS cert store (#976) (#992)
Windows users behind a corporate/antivirus TLS-inspecting proxy got a raw `[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE]` on every model install — the TCP connection reaches the server fine, but the handshake fails because the OS trusts the proxy's re-signed root CA and Python's bundled certifi CA list doesn't. A genuinely different failure mode from #984 (that was TCP-level unreachability to a blocked host, before any TLS negotiation). - backend/core/failure.py: new SSL_HANDSHAKE_FAILURE classification (handshake/cert-verify-failed/sslv3_alert/sslcertverificationerror substring markers) with an actionable hint, added to _CONTEXT_FREE_HINT_CLASSES so append_hint() (already called by setup/download.py's install worker) surfaces it without further wiring. - backend/main.py: truststore.inject_into_ssl() at module level, before any huggingface_hub/requests/httpx network I/O — patches ssl.SSLContext to verify against the OS trust store instead of only certifi's bundled CA list. Not platform-gated (correctness improvement everywhere); wrapped in try/except so it never blocks startup. - pyproject.toml/uv.lock: truststore>=0.9 — pure Python, MIT, PyPA- maintained, zero transitive deps, same class of fix as socksio. Verified: uv lock --check + uv sync --frozen clean (lockfile diff is just the one new package); main.py imports cleanly; full backend suite passes; no hiddenimports entry needed (main.py is PyInstaller's direct entry script per backend.spec, so a top-level import traces normally — unlike socksio's case, which was httpx's internal lazy import). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
324d417d27 |
fix(engines): mlx-audio no longer crashes on unsupported languages, error messages never leak raw exception internals (#977) (#993)
Root cause: MLXAudioBackend.generate() blindly truncated the full
language display name to two characters (language[:2].lower()),
assuming an ISO code — 'Dutch' -> 'du', which crashed Kokoro's vendored
pipeline's internal assertion (assert lang_code in LANG_CODES, (lang_code,
LANG_CODES)) for any language whose first two letters didn't coincidentally
match one of Kokoro's single-letter codes. The raw AssertionError's
tuple-containing-a-dict args then leaked straight into the user-facing
500 message via two stacked f"...{e}" formatters in generation.py.
- resolve_kokoro_lang_code() resolves against the AUTHORITATIVE
ALIASES/LANG_CODES table read from the installed mlx_audio package
(never a hardcoded guess), and only applies when Kokoro is the actual
active curated model — other curated models (CSM, Dia, Qwen3-TTS,
OuteTTS, ...) either ignore the kwarg or expect a different format, so
Kokoro's strict validation doesn't wrongly reject them. Unsupported
languages now raise a clear ValueError naming what Kokoro supports,
which generation.py already converts to a clean 400.
- _safe_exc_text() hardens both generic exception formatters in
generation.py: if any element of an exception's .args is a container
(dict/list/tuple/set), never interpolate str(e) raw — name the
exception type and point at the log instead. Protects every current
and future engine's generate() from leaking a raw container repr, not
just this one Kokoro assertion.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
637020b82b |
fix(engines): nemo-parakeet install hint stops recommending a shared-venv-breaking pip install (#974) (#991)
The Engines page told users to run `pip install nemo_toolkit[asr]` for the NeMo Parakeet ASR engine. nemo_toolkit[asr]==2.7.3 hard-pins transformers>=4.57,<4.58, which is unsatisfiable alongside OmniVoice's own transformers>=5.3 requirement (needed by omnivoice/models/omnivoice.py for HiggsAudioV2TokenizerModel). A user who followed the hint ended up with a backend that wouldn't start (ImportError: cannot import name 'HiggsAudioV2TokenizerModel'). _INSTALL_HINTS["nemo-parakeet"] in backend/services/asr_backend.py now states plainly that installing into the shared venv will break the backend, names the transformers conflict, and tells users to use a separate/dedicated Python environment instead — without implying a safe one-line fix or an isolated-venv env var exists (unlike dots-tts/moss-tts-v15/confucius4-tts, nemo-parakeet has no isolated venv option yet; that's a separate, larger follow-up). Also adds one sentence to docs/install/troubleshooting.md's existing "engine venv clash" section (#11) pointing at the same class of issue on the ASR side, and a regression test asserting the hint never again contains the literal bare `pip install nemo_toolkit[asr]` string. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
dab6456581 |
docs(linux): stop advertising a .deb package that isn't published (#990)
README's Quickstart badges linked a 'Download Debian .deb' button straight to the releases page — but .deb bundling was deliberately dropped from release.yml (tauri-cli bug, 'Failed to create control scripts') and no release has ever shipped one. A community member investigating #961 confirmed this by checking the actual release assets. Users clicking that badge got a broken promise, not a package. Removed the badge; docs/install/linux.md's '## Install (.deb)' section now honestly states it's unavailable pending a tauri-cli fix, points to the AppImage as the supported path, and keeps the historical pre-v0.3 .deb upgrade note (ffprobe conflict) since that's still relevant to existing installs. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
015d29cafb |
test(voice-design): lock in RTL/non-Latin script handling for the instruct field (#980) (#989)
Issue #980 reported a raw 400 for Hebrew text ('שמואל') typed into the
Clone tab's Style field. Investigated: this is the same failure class
as #612 (Vietnamese free-text) and was already fixed when #612 landed
in commit
|
||
|
|
1a03c59f82 |
fix(install): AMD ROCm torch reinstall targets rocm6.4, not rocm6.2 (#988)
Community-diagnosed (issue #972, Kaihui-AMD): pyproject.toml pins torch==2.8.0, but the rocm6.2 wheel index only ever published up to 2.5.1 — the reinstall silently failed to resolve and fell back to the default CUDA build, which runs on CPU on an AMD GPU. The failure was correctly logged (bootstrap.rs's emit_log warning), just never actioned because the index itself couldn't succeed. rocm6.4 carries a matching torch==2.8.0 build. Docs updated with the corrected index plus a repo.radeon.com find-links path for users who want a driver-matched ROCm 7.2.x build the PyTorch index doesn't carry (OMNIVOICE_TORCH_INDEX only accepts a PEP 503 index, not find-links, so that's documented as a manual step). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8281b7c798 |
fix(engines): dub and batch TTS honor the active-engine selection (#987)
* fix(engines): dub and batch TTS honor the active-engine selection — with a real capability gate, not a silent OmniVoice fallback
Dub generation and batch TTS hardcoded services.model_manager.get_model()
(OmniVoice) regardless of the engine picked in Settings → Engines. A user
selecting VoxCPM2 (or any other engine) still got OmniVoice output with no
error — the silent fallback IS the bug class, not just the one report.
Root-caused and fixed for the whole class:
- New `TTSBackend.supports_cloning` capability flag (default True) marks
engines that can only offer fixed preset voices — kittentts,
supertonic3, sherpa-onnx set it False. MLXAudioBackend exposes it as an
instance property (Kokoro doesn't clone, CSM does) since the adapter
multiplexes multiple models with different capabilities.
- `cloning_capable_engine_ids()` and a shared `resolve_generation_backend()`
helper in services/tts_backend.py centralize engine resolution
(id → is_available() → routing gate → optional cloning gate), mirroring
generation.py's /generate resolution instead of inventing a third
parallel mechanism. Both routers now standardize on the existing
get_active_tts_backend() cache (unload-on-switch already handled).
- dub_generate.py's two TTS-generate call sites (main run + OOM retry) and
the /dub/preview-segment route resolve once, up front, with
require_cloning=True — dub's ref_audio is populated for essentially
every real job, so an engine that can't clone fails the whole job with
one actionable message instead of mis-cloning per segment.
- batch.py resolves once per job, require_cloning only when voice_id is
pinned — an unpinned batch job runs fine on any engine.
- Applied the three pre-existing TODO(#312) comments: mastering now skips
via `applies_own_mastering` for both pipelines, matching generation.py.
Regression tests cover the capability-id list, the fail-fast gate (proving
no OmniVoice fallback), the success path on a selected non-OmniVoice
engine, batch's pinned-vs-unpinned voice_id behavior, and the mastering
skip for both pipelines. Three existing dub tests that mocked get_model()
directly were updated to mock the new resolver instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(engines): exclude model-dependent adapters from cloning_capable_engine_ids()
getattr(cls, "supports_cloning", True) at the CLASS level returns a
property descriptor object (always truthy) when the flag is an instance
@property, not a plain attribute — MLXAudioBackend uses exactly this
pattern because its cloning capability depends on which of its 7+ curated
models is loaded (only CSM clones; Kokoro etc. don't). Without this fix,
the dub/batch capability-gate error message would always recommend
'switch to mlx-audio' even when the user's configured MLX model can't
clone, sending them in a circle back to the same error.
isinstance(value, bool) distinguishes a resolved boolean from a
descriptor object, so mlx-audio is excluded from the suggestion list
until its actual per-instance capability can be checked (already handled
correctly by resolve_generation_backend()'s per-call instance check).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): engine-aware dub/batch entry (#987)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
|
||
|
|
efe57ee49d |
fix(ui): loaded-models panel says when a resident model is not the active engine (#985)
* fix(ui): loaded-models panel says when a resident model is not the active engine (#TBD) A switched-away TTS model stays resident in VRAM until Unload/Flush or the idle evictor, so users who picked another engine (e.g. voxcpm2) saw "OmniVoice TTS - cuda:0 - 1937 MB" in the LOADED MODELS flyout and concluded synthesis was still routing to OmniVoice. It wasn't - the panel just gave no hint that resident != active. /model/loaded entries for TTS-family models (in-process OmniVoice + subprocess sidecars) now carry engine_id + is_active_engine, computed against active_backend_id(); attribution failure degrades to the old shape (is_active_engine: null) and non-TTS entries (ASR, diarization) are left unannotated. The flyout renders a muted "not active - safe to unload" tag (i18n: header.model_not_active, en + zh-CN) on inactive entries; Unload/Flush behavior is unchanged. Regression tests cover both attribution states, the ASR non-label, and the degradation path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): loaded-models active-engine hint entry (#985) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mm2): string-target monkeypatch for active_backend_id — immune to sys.modules reimports The two attribution tests patched the collection-time module alias; other suites pop+reimport services.* modules mid-run, so in full-suite order the patch landed on a stale module object while _active_tts_id late-imported the fresh one (CI-only failure). String targets resolve at patch time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
7f77f4d7bf |
fix(audio): remove hidden reverb from the mastering pre-stage — reverb is preset-declared only (#986)
* fix(audio): remove hidden reverb from the mastering pre-stage — reverb is preset-declared only (#TBD) Field report (Discord): baked-in echo/reverb on some voices. apply_mastering() hardcoded a Reverb that ran on every non-raw synthesis before the user's preset chain — broadcast shipped reverb it never declared, podcast broke its "no reverb" promise, cinematic/warm got doubled reverb. The mastering pre-stage is now data-driven (MASTERING_CHAIN: highpass + compressor, same params as before) and reverb-free; cinematic/warm keep their user-chosen reverb. Regression tests pin the contract, incl. a burst-then- silence echo-tail check and pedalboard-missing passthrough. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): hidden mastering reverb entry (#986) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
087309259b |
fix(setup): first-run network check is mirror-aware and never hard-blocks (#984)
* fix(setup): first-run network check is mirror-aware and never hard-blocks Field report (Discord, China): the Launchpad preflight probed hardcoded huggingface.co:443 and any failure disabled Continue outright — users behind the GFW were stuck on the very first screen, before Settings (and its HF mirror quick-pick) was even reachable. - The probe now targets the HF endpoint actually in effect (HF_ENDPOINT / hf_endpoint pref via configured_hf_mirror), with the real port. - An unreachable endpoint is a WARNING, not a blocker: local-first — cached models work offline, and downloads surface their own actionable errors. - When huggingface.co is blocked but hf-mirror.com answers, the fix text says exactly that, and the wizard shows an inline mirror quick-pick (presets + custom URL) that applies via PUT /hf-mirror — effective immediately for downloads — then re-checks. - Docs updated (downloading-models, install troubleshooting); regression tests cover warn-not-fail, mirror-host probing, and the mirror suggestion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): open [Unreleased] with the preflight mirror fix (#984) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
453db55f12 |
release: freeze v0.3.11 — version bump, lockfiles, changelog (#970)
package.json + three mirrors -> 0.3.11 in lockstep; Cargo.lock/uv.lock/ bun.lock regenerated; CHANGELOG [Unreleased] -> [0.3.11] — 2026-07-05 with the multi-language-release headline; nine entries since v0.3.10. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>v0.3.11 |
||
|
|
620321a9cc |
feat(diagnostics): backend crashes become self-documenting — exit code + stderr tail surfaced and attached to bug reports (#969)
* feat(diagnostics): backend crashes become self-documenting — exit code + stderr tail surfaced and attached to bug reports When the backend PROCESS died (native CUDA abort, OOM kill, DLL crash) the user saw only "Can't reach the local OmniVoice backend" and the evidence died with the process — every #941-class report needed a logs-please round-trip nobody answers. The v0.3.9 guard fixed HANGS; this fixes the class of invisible DEATHS: - Rust (crash.rs): every unexpected child exit — detected by the startup health poll and the post-Ready supervisor — writes a rotating (last 3) JSON crash marker next to the backend logs: ts, exit code/signal, backend version, uptime, ~40-line stderr tail. Intentional shutdowns never forensicate: app-quit raises the quitting flag first (now also on macOS Cmd+Q via ExitRequested), and retry/clean-retry kills set a BACKEND_KILL_INTENDED flag cleared when the fresh child is tracked. - Tauri commands get_last_backend_crash / acknowledge_backend_crash; ack is a persisted watermark, never a delete — bug reports still get the evidence after the user viewed it. - Crash-loop escalation: the supervisor budget goes 5-in-60s → 3-in-10min so slow crash loops stop respawning and land on the Failed screen with the last exit code + stderr tail. - Frontend: apiFetch's transport-failure path swaps the vague message for "the backend crashed (exit code X) N s ago…" when an unacknowledged marker exists, and BackendCrashNotice (banner + details dialog, i18n'd, ack-on-view) surfaces it even with no request in flight. - Bug-report prefill gains a "Last backend crash" section (exit code + home-path-scrubbed stderr tail via the existing scrubText), so the next report arrives WITH the evidence. Tests: cargo --lib 57 pass (marker rotation write-4-keep-3, ack semantics, store IO, ExitStatus decomposition, 3-in-10min policy); vitest 909 pass incl. crash-notice branch, client crash-message branch, bug-report enrichment; legacy node:test 41 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add backend crash forensics under [Unreleased] (#969) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
738d45f1c7 |
fix(ui): timeline box colors pre-blended in JS — visible on any WebView2 (#963) (#968)
* fix(ui): timeline box colors pre-blended in JS — visible on any WebView2, color-mix dependency removed (#963) #951 moved the segment-box palette to `color-mix(in srgb, tint 45%, var(--chrome-bg))` strings applied as inline styles. WebView2/Chromium < 111 has no color-mix, so the CSSOM rejects the whole `background` assignment — and since .seg-track__box declares no background of its own, the boxes rendered fully transparent on pinned/enterprise WebView2 runtimes (the Windows installer never enforces a minimum runtime). Fix the class, not the instance: no engine-dependent CSS may reach this lane's inline styles. The 0.45·tint + 0.55·bg blend now happens in JS — timeline.js keeps the tints as numeric [r,g,b], reads --chrome-bg off the document root (fallback #0f1011), and emits literal `rgb(r, g, b)` strings every engine parses. Pixel-identical to what color-mix painted. Theme-awareness is preserved by re-blending when [data-theme] changes on <html> (the seam App.jsx switches themes through), observed via MutationObserver; SegmentTrack subscribes with useSyncExternalStore so mounted boxes recolor live. Guards updated: palette entries must match plain opaque rgb() (no color-mix/var()/alpha), the default-theme blend is asserted against independently computed literals, theme-change re-blend and rgb()/ garbage --chrome-bg parsing are covered, and SegmentTrack's rendered inline background is asserted to be a literal rgb() — fails on any reintroduction of engine-dependent CSS in this lane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add WebView2 box-color fix under [Unreleased] (#968) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d959aae41b |
fix(dub): dialogue starts stop snapping to footsteps — sustained-energy onsets, bounded snap (#963) (#967)
* fix(dub): dialogue starts stop snapping to footsteps — sustained-energy onsets, bounded snap distance (#963) Field report #963 (point 3): dubbed speakers start seconds early or late. The reporter's own theory was right on the money — 'when a noise is heard (a sigh or footsteps), it's interpreted as the start of the conversation.' The #280 onset snapper took the FIRST 20 ms frame above an adaptive RMS threshold as the speech onset, so any transient qualified; it also had no snap-distance bound (a wrong onset could move a start by the whole segment minus 0.3 s) and ran even when Demucs had failed and the 'vocals' track was really the raw mix, where every ambient sound is a candidate. Three layered guards, all pure NumPy (no new deps): - Sustained energy: an onset only counts when >=160 ms of the following 300 ms stays above the threshold. Footsteps/door thuds light up one or two frames and die; syllables keep the energy up. - Bounded snap distance: shifts beyond 1.5 s are only trusted when the skipped span is (near-)silent — that is exactly the genuine #280 whisper start-stretch on the vocals track (Demucs removed the music, leaving real silence), so long trims over silence still work in full. Long jumps over audible content (e.g. quiet speech under the relative threshold) are refused instead of playing the dub seconds late; an isolated transient in the span (<10% audible frames) doesn't block it. - Source-aware: snapping now runs only on the separated vocals track. dub_core detects the Demucs fallback (vocals_path == audio_path, see dub_pipeline) at both call sites and passes separated_vocals=False on mixed audio, disabling snapping — whisper's own timestamps beat a confidently wrong snap when music/ambience is sustained energy too. Tests (tests/test_onset_align.py, fail-before/pass-after): transient burst rejected at detect- and snap-level, transient-only window yields no onset, long jump over audible content refused, bounded shift over audible lead still allowed, >1.5 s trim over true silence still snaps (#280 regression guard), mixed-audio mode is a no-op. 28 pass in the file; full dub-adjacent suites green. Credit: theory and repro description by the #963 reporter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add onset-snap robustness under [Unreleased] (#967) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
de99bc3bd5 |
fix(net): SOCKS-proxy users can synthesize again — ship socksio, cache-first model resolution (#959) (#966)
* fix(net): SOCKS-proxy users can synthesize again — ship socksio, cache-first model resolution, degrade LLM clients (#959) Under ALL_PROXY/HTTPS_PROXY=socks5:// without socksio installed, httpx raises ImportError AT CLIENT CONSTRUCTION ("Using SOCKS proxy, but the 'socksio' package is not installed"). huggingface_hub's get_session() builds exactly that client inside snapshot_download, so POST /generate 500'd with the bare message even for a fully installed model, and preload_model's model_info probe hit the same error and silently skipped warm-up. Latent since v0.3.5 — #947's fresh-process engine spawning unmasked it in v0.3.10 by handing the user's proxy env directly to a clean backend process. Three layers, so the class (any session-construction failure) is dead, not just the reported instance: * Ship SOCKS support: socksio>=1.0 in [project] dependencies (pure Python, MIT, zero transitive deps) AND in backend.spec hiddenimports — httpx imports it lazily in try/except, so PyInstaller's tracer misses it and the frozen installers would stay broken without the explicit entry. uv.lock regenerated; `uv lock --check` and `uv sync --frozen` (the Docker/release bootstrap semantics) verified. * Cache-first model resolution: from_pretrained's snapshot resolution extracted into _resolve_snapshot_dir() — local dir, else snapshot_download(local_files_only=True) (a complete cache resolves with NO HTTP session constructed), else the original network path. preload_model's failed network probe now falls back to a cache-only check and warms up anyway instead of silently skipping (honest log either way). * Class guards: resolve_skill_client wraps OpenAI() construction — env-shaped construction failures degrade to the existing "LLM unavailable" contract instead of 500ing the calling feature; and core.failure learns SOCKS_PROXY_SUPPORT_MISSING with an actionable hint, appended on the raw-string surfaces (global 500 handler, model-install SSE) via the new append_hint(). Fail-before/pass-after verified by reverting the fix: 11 of the 12 new tests fail pre-fix (the remaining one is the unchanged network-fallback contract). 165 tests green across the touched suites. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add SOCKS-proxy resilience under [Unreleased] (#966) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c47633a409 |
fix(settings): a saved LLM provider survives restart — explicit save activates, stale TRANSLATE_* prefs stop hijacking (#963) (#965)
* fix(settings): a saved LLM provider survives restart — explicit save activates, stale TRANSLATE_* prefs stop hijacking (#963) "Ollama works until I restart OmniVoice" had three stacked causes: 1. Only "Save & use for translation" persisted the selection. Plain "Save" and "Test" sent make_active:false, and on restart active_provider_id() deliberately excludes local providers (Ollama/LM Studio) from auto-select — so a saved-and-tested Ollama was never resolved active again. The PUT handler now also claims the active slot on an explicit save when the user has never chosen a provider (new llm_providers.stored_active_provider_id(): the stored row only — no env pin, no legacy fallback, no auto-detect). An explicit prior choice is never stolen; an unconfigured provider can't claim the slot; make_active:true still flips. 2. Users of the retired (≤v0.3.7) Translation-LLM panel had env.TRANSLATE_* rows in prefs.json, re-imported into os.environ every launch — and a live TRANSLATE_BASE_URL resolves the active provider to "custom" ahead of auto-select on every restart. New startup migration (llm_providers.migrate_legacy_translate_prefs, run in main.py BEFORE the prefs→env import) moves those values into the custom provider's own settings-store rows (only where the store has no value yet) and deletes the prefs rows. Real process env vars are never touched; a failed store write keeps the prefs row and retries next boot. The legacy endpoint keeps working — via the store, without hijacking the active slot. 3. The panel read as "done" after a green Test even when another provider stayed active. It now shows a notice after save/Test when the edited provider is not the effective active one (suppressed while LLM_DEFAULT_PROVIDER pins the choice — the env banner already covers that). Tests (fail-before): 7 new backend tests fail on the old code (save-activates, never-steals, migration semantics, env untouched, end-to-end ollama-beats-legacy-env) and the new panel test fails without the notice; all pass after. Full LLM/settings suites, frontend vitest (890), typecheck:ci, oxlint, oxfmt and vite build are green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add LLM-provider persistence fix under [Unreleased] (#965) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4aa9abe22a |
docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962) (#964)
* docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add the install-fixes batch under [Unreleased] (#964) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d90cfde1bb |
feat(dub): per-language translations + per-track caches — switching languages stops destroying work (P1) (#958)
* feat(dub): per-language translations + per-track caches — switching languages stops destroying work (P1) Multi-language dubbing translated per language (#957) but stored everything in single-slot state, so tracks silently destroyed each other's work: P1.2 — per-language translation storage (additive): - Frontend keeps every translation in s.translations[langCode] alongside the legacy s.text slot (still = the shown language). Translate All writes both; the new store action switchDubLangCode swaps text through the map on a user-driven language switch (non-destructive; restore paths keep the plain setter); manual edits / restore-original update the current language's entry; merge joins per-language texts, split drops them. Rides project save/load inside dubSegments — legacy projects behave exactly as before. - Backend mirrors it as job["segments_i18n"] = {lang: {segKey: text}} (segKey = stable id, index for id-less legacy rows), written by _sync_job_segments; job["segments"] stays byte-identical for every existing consumer. /dub/srt|vtt?lang= and subtitle burn-in now emit THAT language's text when present — ExportModal's "all dubs" batch stops producing N identical files. Legacy jobs without the field fall back to today's output. P1.3 — per-track WAV cache + fingerprints: - Per-segment WAVs are language-keyed (seg_{lang}_{id}.wav). The partial-regen read path falls back to legacy seg_{id}.wav ONLY while the job has no other-language track — single-language jobs keep their whole on-disk cache; multi-track jobs stop splicing the last-generated language into the current track. Read-only endpoints (segment preview, clips zip) gained ?lang= with the permissive legacy fallback they always had. - Fingerprints include the track language (segment_fingerprint(track_lang=…), /tools/incremental lang=…) and live in job["seg_hashes_by_lang"]; the flat job["seg_hashes"] stays as the current track's mirror so the done event, history restore and older frontends read it unchanged. A legacy flat map is attributed to the job's last-generated language (dropped when unknown) and reads stale once — the safe direction. seg_wav_kind is per-track too. - The frontend stores fingerprints per language and judges "Regen N changed" against the ACTIVE track; project save/load and dub-history restore carry all tracks' hashes (segHashesByLang / seg_hashes_by_lang, additive). Tests: fail-before regression coverage — two-track regen never splices the other language's audio (sample-level assert on the mixed track), legacy single-track cache reuse + multi-track gate, per-lang seg_hashes with flat mirror + migration semantics, /dub/srt|vtt?lang= emitting different text per track with legacy fallbacks, per-lang burn-in, /tools/incremental lang scoping, and 14 frontend tests for translations round-trips, per-track fingerprints and legacy-project behaviour. Full backend + frontend suites, typecheck, lint and format:check green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add per-language storage + per-track caches under [Unreleased] (#958) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bd92729623 |
feat(dub): multi-language generate translates each language first + picks persist (P1) (#957)
* feat(dub): multi-language generate translates each language first + picks persist with the project
P1.1 — the "Generate N dubs" loop never translated: the backend synthesizes
segment text verbatim, so every multi-language track rendered the same words
and at most one was actually in its language. The loop now runs
translate → generate per pick:
- handleTranslateAll(langOverride?) accepts an optional ISO-code override
(no-arg Translate All behavior unchanged; a click-event first arg is
guarded). It resolves true only when a translation actually landed, and
both it and handleDubGenerate snapshot segments from the store at call
time — the click-time closures went stale the moment the previous pick's
translate pass rewrote the segments.
- A pick whose translate fails (request error or all-segments-errored) is
SKIPPED — never a wrong-language track — the batch continues, and the
skipped languages are reported in a final toast.
- The redundant first translate is skipped only when pick 1 targets the
language the editor text is already translated into; every later pick
always translates.
- Honest progress: the pill shows "Translating → {lang} (i/N)…" before each
generate, and the header CTA is inert while translating so a re-click
can't start a second batch (belt: a ref guard in the loop).
P1.4 — multiLangMode/multiLangs move from DubTab component state into the
dub store slice and ride the project save/load payload (exportTracks too).
Additive and back-compat: legacy payloads default to off/empty and leave the
in-session exportTracks untouched (utils/projectState.js).
Tests (fail-before verified: 9 failures on the pre-fix code):
- handleTranslateAll override targets + return semantics + call-time
segment snapshot (dubTranslateAllOverride.test.jsx)
- per-language translate-before-generate call order, skip-on-failure with
continuation + skip-report toast, first-pick skip heuristic, unchanged
single-language path (dubMultiLangGenerate.test.jsx)
- slice defaults/setters/reset, payload round-trip, legacy-payload defaults,
App.jsx wiring guards (dubMultiLangPersist.test.js)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): add multi-lang auto-translate under [Unreleased] (#957)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
afe819f498 |
fix(ci): oxfmt the two #956 test files (unbreak main format check)
#956 merged with a red Tests gate — my merge script ran unconditionally instead of aborting on the gate value; the failure was oxfmt-only on the two new test files. Whitespace-only fix, tests re-verified green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6b91205036 |
fix(dub): completed tracks always show their tabs + history keeps its language (P0) (#956)
* fix(dub): completed tracks always show their tabs + history keeps its language (P0)
Root cause chain: the track switcher's visibility expression required
dubLangCode !== 'und' and ended in a tautology (dubTracks?.length > 0 ||
!!dubTracks), so it was effectively keyed to the language dropdown, not
the persisted tracks. History restore always handed the frontend 'und'
because the dub_history language/language_code COLUMNS froze at the
ingest-time "" — the save_job UPSERT never updated them after generation
set them on the job dict (only the job_data JSON carried the real value).
Net effect: a restored project with finished tracks showed no track tabs
until the user re-picked a language.
- DubTab: hasDubbedTrack = done && dubTracks.length > 0 (tracks only;
also stops the tautology from showing a trackless switcher).
- DubTab auto-jump: membership-guarded — the preview only jumps to a
language that has a track, else tracks[0]. Kills the preview-404 class
(restores falling back to 'en' with tracks ['bn'] pointed the player
at /dub/preview-video?lang=en).
- dub_pipeline.save_job UPSERT: language/language_code now update when
non-empty (same CASE guard as content_hash), so new saves heal the
frozen columns and empty re-saves can't clobber them back.
- App.restoreDubHistory: falls back to job_data's language/language_code
so EXISTING rows in users' DBs restore correctly with no migration.
- P0.2 polish: track pills get duration + timing-strategy tooltips,
hydrated lazily and failure-silently from the existing
GET /dub/tracks/{job_id} via new api/dub.dubListTracks; all new
strings through i18n (en.json).
Tests (fail-before/pass-after): DubTab-level visibility + auto-jump
membership-guard tests (3 of 4 fail pre-fix), pill-tooltip hydration
tests, and save_job language heal/no-clobber tests (heal fails pre-fix).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): open [Unreleased] with the dub track-tabs fix (#956)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1dfa032be4 |
feat(dub): project title first in the editor header, pipeline stages below (#955)
Header reordered per owner: row 1 = title (+ duration/segments) with the action buttons, row 2 = the Upload→Export pipeline spine directly beneath with a tight 2px gap (was: stepper and title side-by-side on one row). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fd7d20fe1e |
release: freeze v0.3.10 — version bump, lockfiles, changelog (#954)
package.json (source of truth) + the three mirrors -> 0.3.10, in lockstep; Cargo.lock/uv.lock/bun.lock regenerated (one line each; bun --frozen-lockfile verified). CHANGELOG [Unreleased] -> [0.3.10] — 2026-07-05 with the release headline; nine fixes since v0.3.9, mostly same-day field-report turnarounds. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>v0.3.10 |
||
|
|
27ff5846c2 |
fix(audiobook): chapter render no longer crashes on mixed 1-D/2-D audio chunks (#897) (#953)
* fix(audiobook): chapter render no longer crashes on mixed 1-D/2-D audio chunks (#897) Root cause: synthesize_chapter (backend/services/audiobook.py) built inter-span pause silence as bare 1-D torch.zeros(n) while every real engine's synth returns (1, samples) per the TTSBackend.generate contract (OmniVoice's model.generate(...)[0] included) — so the chapter's final hard concat in chunked_tts.concatenate_audio_chunks hit torch.cat with mixed ranks and died with 'RuntimeError: Tensors must have same number of dimensions: got 1 and 2'. Any chapter containing a [pause] span (Stories/audiobook longform) crashed; existing tests missed it because their stub synth returned 1-D. The crossfade branch had the same latent bug for mixed-rank chunks. Fix, both layers: - concatenate_audio_chunks now normalizes chunk shapes before any cat (_normalize_chunk_shapes): lower-rank chunks gain leading singleton dims to the highest rank present, then singleton channel dims broadcast to the widest channel count (mono follows stereo). Covers both the hard-cut and crossfade branches; homogeneous input passes through untouched, so all-1-D / all-2-D callers keep their exact output shapes. No future backend's output rank can re-break the join. - synthesize_chapter materializes silence AFTER the loop, matching the rendered audio's channel dims / dtype / device — the same pattern generation.py's _render_with_pauses already uses for the single-shot path — so the data is rank-consistent at the source too. A silence-only chapter stays 1-D float32 as before. Regression tests: mixed-rank hard-cut (both orders), mixed-rank crossfade, mono->stereo broadcast, all-1-D/all-2-D shape stability, a 2-D-engine + [pause] chapter through synthesize_chapter (the exact #897 scenario), and a spy asserting the parts reaching the concat are rank-homogeneous. All fail before the fix with the reported error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add the audiobook pause-span concat fix under [Unreleased] (#953) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d14b37fab2 |
fix(dub): the speaker-count hint is honored on every diarization path + clone-purity guard (#952)
* fix(dub): the speaker-count hint is honored on every diarization path + clone-purity guard
The dub "Speakers" count reached _diarize() and then died on 3 of its 4
branches, so setting it changed nothing, speakers blended, and auto-clones
were cut from mixed-speaker audio ("made up" voices):
- FunASR inline-turns shortcut returned before the hint was ever consulted
→ now an explicit num_speakers routes the job through pyannote (the one
engine that honors an exact count); turns stay the fast path only when no
hint is set, and remain the fallback (with an honest "hint ignored"
warning) when pyannote can't load or crashes mid-run.
- pyannote-unavailable fallback used a hardcoded 2-speaker silence-gap
heuristic → assign_speakers_heuristic now takes num_speakers and cycles N
labels on gap boundaries (1 → single speaker; None → legacy alternation),
and the existing diarization warning says the hint is only approximately
honored.
- pyannote-crash fallback dropped the hint the same way → same treatment.
No branch drops the hint silently anymore: every degraded path extends the
existing `warning` SSE payload (detail + a machine-readable speaker_hint
field) that the frontend already renders.
Parity + purity:
- POST /dub/transcribe/{job_id} (the CLI's endpoint) gains the same clamped
num_speakers query param, forwarded to pyannote and the heuristic; the
omnivoice-dub CLI gains --speakers N.
- Clone-purity guard: _pick_reference_slices rejects sub-1.5s slices, prefers
slices not temporally adjacent (<0.3s) to another speaker's turn (scoring
preference, not a hard filter), and extract_speaker_clones skips extraction
entirely when labels came from the heuristic (labels_source kwarg threaded
from _diarize; missing kwarg keeps the old behavior) — with a user-facing
warning pointing at Settings → Models → pyannote.
Tests: fail-before/pass-after coverage in tests/test_speaker_hint.py (all
four _diarize branches driven through the real SSE stream), clone-purity
guards in tests/test_speaker_clone_purity.py, heuristic hint semantics in
tests/test_segmentation.py.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs(changelog): add the speaker-hint + clone-purity fix under [Unreleased] (#952)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
edf86c1800 |
fix(ui): dub timeline boxes can no longer flash invisible during playback (#373 class, completes #381) (#951)
* fix(ui): dub timeline boxes can no longer flash invisible during playback (#373 class, completes #381) Root cause: the segment lane animated an inline `transform: translateX(-scrollLeft)` on every playback tick, so Chromium promoted it to a compositor layer — and on some Windows GPU/WebView2 driver combos, composited semi-transparent paints (the 0.45-alpha box fills) flash invisible/visible while the layer moves, settling only when paused. PR #381 removed `will-change` and raised the alpha, which only dampened the symptom; the animated transform kept the lane composited. Fix the class — no composited translucent paints on the lane, ever: - Position boxes in pure layout: the lane transform is gone; each box's `left` is start·pxPerSec − scrollLeft (viewport coordinates). The virtualization window already derives from the same scrollLeft, so windowing stays consistent by construction. The selfScroll WebKit fallback keeps lane coordinates (its viewport is a real scroll container), unchanged. - Belt-and-braces: REGION_COLORS are now fully opaque — each entry pre-blends the old 45% tint against the surface behind the lane (`--chrome-bg`, the .studio-panel background) via color-mix, which is pixel-identical to the previous alpha compositing (0.45·tint + 0.55·bg) in every theme, with zero alpha. Regression tests (fail on pre-fix code): lane carries no transform at rest and after a scroll update, box lefts are viewport-relative for a scrolled view, no double-shift in the selfScroll fallback, and every REGION_COLORS entry is alpha-free with the 45% ratio preserved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add the timeline-box compositor fix under [Unreleased] (#951) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
437a995a0f |
fix(translate): Cinematic/Autofit can no longer invent dialogue — divergence guard + pinned temperature (#950)
Root cause (v0.3.9 field report): the refine paths had no-op output guards. Cinematic's ADAPT step only checked _looks_like_target_script, which returns True unconditionally for every Latin-script target (no _SCRIPT_RANGES entry) — so any non-empty LLM reply (hallucinated dialogue, refusals, commentary, or the REFLECT critique itself) shipped as the dub line. Autofit's adjust_for_slot accepted ANY non-empty reply, and its best-candidate tracker (closest rate_ratio to 1.0) actively selected the most-padded output, while _EXPAND_PROMPT invited invention with no ceiling. Both call paths also ran at the provider-default temperature 1.0, unlike the working Fast path which pins 0.2. The fix, class-level: - Shared divergence guard translator.refine_output_ok (length window 0.4–2.5x, env-tunable via OMNIVOICE_REFINE_RATIO_MIN/MAX, with an absolute cap for short references; target-script check; critique-echo detection). Rejected ADAPT output degrades to the literal with error="adapt-diverged" (wrong-script keeps its adapt-wrong-script:<lang> marker), riding the existing degradation machinery unchanged. - Autofit validates every reply against the ORIGINAL input text (divergence compounds across attempts otherwise); rejected candidates are discarded (attempt burned, graceful degradation to the input preserved) with error="fit-diverged"; lines under 15% of their slot skip LLM expansion entirely (fit-skip-short) — they could only "fill" the slot with fabricated dialogue. - temperature=0.2 pinned on the cinematic (_chat) and fit (llm.chat) calls; chat/chat_messages gained an optional temperature param that is only sent when set, so refinement/director/glossary callers keep provider defaults. - Prompts hardened: ADAPT forbids introducing facts/names/dialogue not in the source line; EXPAND forbids inventing information and more than doubling the line. - speech_rate strict-mode docstring made honest: strict changes only the upper tolerance bound; expansion still runs (now guard-bounded). Fail-before/pass-after regression tests for the reported bugs (10x runaway ADAPT on an es target, critique echo, hallucinated slot-fill expansion, refusal replies, tiny-line expansion skip, pinned temperature) plus the previously-untested wrong-script fallback and legit-output acceptance. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
03eb8f7d04 |
docs(readme): community table lists the Discord server's real channels (#949)
The table described aspirational channels (#showcase/#help/#feature-requests/#dev) that don't exist on the server; it now matches reality (#announcements, #releases+#changelog, #issues and #ideas forums, #discuss-ideas, #general). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
beca8cfea8 |
fix(launcher): replace a stale old-version backend instead of attaching to it (#947)
* fix(launcher): replace a stale old-version backend instead of attaching to it After an update, an orphaned backend from the PREVIOUS version can survive holding the port. It still answers /system/info, so both attach paths (lib.rs launch bootstrap + bootstrap.rs retry) treated it as "already serving OmniVoice — attaching" and the new UI silently ran OLD backend code: every fix in the update appeared to change nothing. Reported on Discord as "a bound port which blocked the newer versions"; the app already knew how to kill_orphan_on_port on both Unix (lsof) and Windows (netstat) — it just never applied it to a healthy-but-stale backend. The attach decision now compares versions: running_backend_version() reads app_version from /system/info (string-sniff, no new deps), and same_app_version() compares BASE versions (pre-release -N suffix stripped, so a preview build 0.3.10-4 still attaches to its 0.3.10 backend). Same version → attach exactly as before. Different or missing version → the orphan is killed and the bundled backend spawns. Foreign processes keep the existing port_in_use take-ownership path; the post-spawn health polls are untouched (we spawned that backend ourselves). Rust unit tests cover the /system/info parse shape and the match/preview/stale/unversioned decisions; 51 pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add the stale-backend port-reclaim fix under [Unreleased] (#947) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
32103ad7b7 |
docs(readme): charm + organization overhaul (Opal-style) — collapsibles + OpenAI-compatible API section (#945)
* docs(readme): charm + organization overhaul (Opal-style) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(readme): restore inventory-exact feature names (docs-drift guard) The charm pass sentence-cased five bold leads in the collapsed feature list; scripts/check-docs-drift.py greps for the inventory's exact title-case names. Restored: Vocal Isolation, Speaker Diarization, Batch Queue, AI Watermark, GPU Auto-Detect. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(readme): dubbing screenshot shows a real completed dub (37 segs, EN→BN) Replaces the empty drop-zone shot with the populated editor — video + waveform + cast, 37 Bengali segment rows, DUB COMPLETE banner — captured live from the v0.3.9 app; caption updated to match. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
46c4445258 |
fix(ui): relative timestamps no longer render as "20617d ago" (seconds-vs-ms class) (#946)
Backend rows store timestamps as Unix SECONDS (time.time() REAL columns:
generation_history, dub_history, exports, longform jobs, projects), while
frontend-local records carry milliseconds (Date.now() story projects) or ISO
strings (transcriptions). Projects/OmniDrive fed the seconds straight into a
millisecond-based diff (fmtTime), so every generation-history card rendered
as ~1970 ("20617d ago") — and, because the same raw value drove the recency
sort, history items also sank to the bottom of the drive.
Fix the class, not the label: a single shared, unit-tolerant normalizer
(frontend/src/utils/relativeTime.js) now backs every relative-time call site.
- toMillis(ts): numbers < 1e12 are seconds (×1000), >= 1e12 already ms; ISO
and numeric strings parse; Date instances pass through; null/0/undefined/
garbage -> null. Backend storage format is untouched (backward compat).
- timeAgo(ts): "—" for missing stamps (never an epoch age), "just now" for
future stamps within 1 min of clock skew, s/m/h/d buckets, short absolute
date beyond 7 days.
- absoluteTime(ts): unit-safe tooltip text, '' when missing (no more
"Jan 1 1970" titles on null rows).
Converted call sites: pages/Projects.jsx (drop local fmtTime + per-source
*1000 juggling; normalize ts once so sort and label agree), components/
Sidebar.jsx + components/WorkspaceProjects.jsx (drop duplicated local
timeAgo copies and caller-side *1000), pages/BatchQueue.jsx (drop local
formatAge; missing created_at used to render an epoch date), pages/
Transcriptions.jsx + components/TranscriptionPicker.jsx (parse via
toMillis, keep their i18n labels; unparseable stamps no longer render
"Invalid Date").
Tests (fail-before/pass-after): utils/relativeTime.test.js covers seconds/
ms/ISO/numeric-string/Date inputs, null/0 -> "—", clock-skew "just now",
and the 1970 regression (a seconds stamp from today must not render as
thousands of days ago); test/ProjectsRelativeTime.test.jsx guards the
OmniDrive wiring end-to-end (seconds created_at renders "2h ago", null
renders "—", mixed-unit sort orders by real recency). Full frontend suite:
106 files / 843 tests green; oxlint, oxfmt, typecheck:ci, node:test green.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
8bbab3fcc3 |
fix(translate): Dub LLM engine runs on the configured LLM provider (new dub_translation skill) (#944)
* fix(translate): the Dub LLM engine now runs on the configured LLM provider Picking "LLM (OpenAI-compatible)" in the Dub tab read only the raw TRANSLATE_* env vars — completely bypassing Settings → LLM Providers, so a provider the user had configured AND tested in-app silently didn't power the engine (empty key → raw 401 per segment). The Cinematic refiner was already rewired through LLM Skills (#910/#912); this closes the gap for direct LLM translation: * new "dub_translation" LLM skill (Settings → LLM Skills) — per-skill provider override → global active provider, same resolution as every other skill; disabled == unconfigured, no new degradation modes * the provider=openai branch resolves through resolve_skill_client(); TRANSLATE_BASE_URL/TRANSLATE_API_KEY/TRANSLATE_MODEL stay working as the power-user override (env-only setups see zero behavior change, except the stale gpt-3.5-turbo default is now gpt-4o-mini, matching the cinematic path) * per-segment calls are now bounded by the LLM timeout (45s default via OMNIVOICE_LLM_TIMEOUT) instead of the SDK's 600s default * fully unconfigured → an up-front actionable 400 naming Settings → LLM Providers / LLM Skills instead of a per-segment 401 * provider-store keys are resolved into the error scrubber so a provider echoing the key can't leak it (parity with the env-key scrub) * translation_engines registry: honest notes + a configured/configured_via stamp on LLM entries so the Engine dropdown can show ready-vs-needs-setup before the user clicks Translate Tests: 4 new (skills-resolved client wins with its model+timeout; 400s name the right settings page for no_provider vs disabled; env fallback keeps working incl. TRANSLATE_MODEL); skills registry coverage updated; existing openai-branch tests routed deterministically through the env branch via the shared fake helper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(changelog): add the dub-translation provider wiring under [Unreleased] (#944) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9ff5f38cb3 |
docs(readme): cross-promote the maker's other local-first projects (Opal, memxt) (#943)
Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
359c70bc21 |
docs(readme): use the Launchpad screenshot as the hero image (#942)
Swap the static social-preview banner for the live v0.3.9 Launchpad shot and drop the now-duplicate Launchpad row from the gallery (shown once). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0579ec91ad |
docs(readme): Opal-style restyle + fresh v0.3.9 screenshots (#937)
- Emoji section headers with explicit <a id> anchors. Emoji breaks GitHub's auto-generated heading slugs, so every in-page nav target keeps a stable explicit anchor (verified all href="#..." resolve). - Refresh the screenshot gallery. The prior set was from April, predating the launchpad / settings / dictation UI overhaul, so it misrepresented the app. Captured fresh at retina from the live v0.3.9 UI and led the gallery with the new Launchpad home: launchpad, studio, voice design, voice gallery, dubbing, engine-compatibility matrix, model store, embedded API reference (Scalar), and the in-app changelog reader. - Fix the stale engine count (11 -> 14 TTS engines) in the comparison table, FAQ, and roadmap to match the engine table + backend registry. - Use <kbd> keycaps for the dictation shortcut (Opal detail). Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b6ec4e23f3 |
fix(engines): snapshot lazy registry keys so /engines can't 500 under concurrency (#940)
* fix(engines): snapshot lazy registry keys so /engines can't 500 under concurrency
`list_backends()` runs in a FastAPI threadpool and iterates the lazy TTS/ASR
registries via `items()` → `__iter__`, which held a *live* `dict.__iter__(self)`
open across each engine's slow `is_available()` probe. Meanwhile the lazy
`__getitem__` resolves a deferred entry by mutating the dict (`self[key] = cls`).
A second concurrent `/engines` request (or any ASR op) materializing the lazy
`faster-whisper-isolated` entry therefore changed the dict size mid-iteration:
RuntimeError: dictionary changed size during iteration
asr_backend.py:1729 list_backends → _REGISTRY.items()
asr_backend.py:1665 __iter__ → for k in dict.__iter__(self)
Both `_LazyRegistry` (TTS) and `_LazyASRRegistry` (ASR) now snapshot their live
keys up front with `list(dict.__iter__(self))` — consumed atomically under the
GIL — so a concurrent lazy insert can no longer trip the iteration. The slow
per-engine probes then run over the snapshot, not the live iterator.
Deterministic fail-before/pass-after regression for both registries:
tests/backend/services/test_lazy_registry_concurrency.py.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(changelog): add the /engines concurrency fix under [Unreleased] (#940)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
eb188931b5 |
fix(dub): classify EINVAL transcribe failures so they stop dead-ending (#763) (#936)
A per-chunk temp-WAV write that fails with OSError EINVAL ("[Errno 22]
Invalid argument") — a missing/read-only/full temp dir, a removed drive,
or antivirus — collapsed into "Transcription produced no segments.
[Errno 22] Invalid argument" with no next step. classify() now names the
class (OS_INVALID_ARGUMENT) so build_failure attaches an actionable
temp-dir/disk/AV hint at the exact surface the streaming dub path already
feeds it (dub_core.py:672) — same treatment the ffmpeg and compute-type
classes get. Fail-before/pass-after regression added; the errno-22 token
keeps it from colliding with the errno-2 transformers-import class.
Also stamps the [0.3.9] CHANGELOG section with today's release date
(2026-07-04) ahead of tagging.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
v0.3.9
|