d91beef0fd314250d8d9b94de86dfea019a8bd96
25
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1515b46adb |
feat(batch): watch-folder auto-ingest (#1768)
Opt-in watch folder on the batch queue: pick a directory once and new videos are auto-enqueued with the last Add-to-queue settings, with pause/stop controls and copy-in-progress protection. Files stream to the loopback backend as bytes; paths never leave the app. Also gives the batch queue a reachable UI entry point and streams multipart uploads to disk. Maintainer fix: the watched directory handle is opened with full share mode on Windows so users can rename or delete the folder while it is watched, matching macOS/Linux behaviour, with a cross-platform regression test. Thanks @mvanhorn! |
||
|
|
89d585a36e |
perf(dub,stream): reuse cached segments, batch the default engine, report real TTFA (#1620)
Reuses verified cached segments, safely batches default-engine dubbing, and reports synthesis-only TTFA/RTF. |
||
|
|
7efae54cf8 | fix: budget the selected engine device | ||
|
|
93616a9c2a | fix(watermark): fail open while pool drains | ||
|
|
41722afe3b |
refactor(launchpad): quieter, borderless design refresh (#1515)
* refactor(launchpad): quieter, borderless design refresh The launchpad carried decoration from an earlier direction: icon chips, corner-hung count badges, a permanently visible filled arrow, uppercase mono card titles, and a dotted stipple divider — plus a frame that had been invisible since the app-wide border tokens were zeroed. Rework it around what the borderless direction actually implies: - Feature tiles get a whisper-faint surface instead of a dead frame, and read as three bands (bare glyph + count / title + arrow / description). `--card-hue` is spent sparingly — the glyph at rest, the surface, count and arrow only once raised. Titles move to sans sentence case; counts are plain tabular numerals. Lift softened 4px -> 2px, coloured glow -> neutral shadow, plus an explicit focus ring and a staggered entrance. - Hero drops the boxed "646" pill and the filled A/B-Compare button for quiet type, with a hairline standing in for the separation. - Section labels trade the dotted stipple for a single fading hairline; rows are transparent until hover and reveal "Open" on hover/focus (it stays in the DOM, so AT and keyboard always reach it). - Hero, tiles, recent files, callout and project lists now share one 1180px column — previously only the top half was capped, so lists ran edge-to-edge on a wide display while the deck stayed centred. Two bugs found and fixed while doing it: - Buttons that had `border border-solid border-transparent` removed fell back to the UA default border and rendered a visible 1px outline. They now carry `border-0` explicitly. - `.lp-animate` used `animation-fill-mode: both`, so after the entrance it kept owning `transform` — and animation-origin declarations outrank normal ones, which silently killed the card hover lift. Now `backwards`, which still holds the from-state through the stagger delay. Also drops CSS the page has not rendered since #904: the cursor-spotlight layer, the breath ring, and the per-card waveform strip. Verified with headless renders at 1600/1280/940 and the empty state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(dictation): decode Wayland portal signals and show the capture pill The GlobalShortcuts portal declares Activated/Deactivated as (o session, s shortcut_id, t timestamp, a{sv} options). We decoded the timestamp as u32, so zbus rejected every signal with Signature mismatch: got `(osta{sv})`, expected `(osua{sv})` and the press was dropped as an invalid signal. Registration succeeded and the desktop even reported the bound chord back, so the hotkey looked wired up while doing nothing at all — on every Wayland compositor, for the whole life of the feature (#1490). Decode the 64-bit timestamp, and keep the 32-bit spelling as a fallback so a non-conforming portal degrades to working rather than to silence. With presses arriving, the second half of the failure showed: nothing had shown the widget window since it became a hidden recorder host, so a capture ran with no pill on screen — and a mic or Accessibility failure rendered into a window nobody could see. Add show_dictation_pill, which bottom-centres the capsule on the monitor under the pointer and shows it without taking focus (Windows keeps SW_SHOWNOACTIVATE so paste still lands in the user's document), and call it from the widget for every state but idle. Wayland denies clients their own placement, so the compositor picks the spot there; the pill still appears. dispatch_dictation_capture now logs whether a press was emitted or queued — a press that reaches Rust and produces nothing was otherwise indistinguishable from one the compositor never delivered. Tests: portal signals decode at both timestamp widths (the 64-bit case fails before this change with the exact production error); pill placement centres, respects a second monitor's origin, and clamps rather than going off-screen; the widget shows for a state needing the user, stays hidden while idle, and never shows for a press that arrives while dictation is disabled. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: sync in-progress workspace changes Uncommitted work already in the tree, checkpointed so the branch matches the local machine: - Remote GPU workers: join-from-the-app flow, one-time secrets, QR join codes, a Compute control in the status bar, and the device-list Workers panel (#1516) - Model Catalogue workspace, with Settings pointing at it - Settings sidebar search and keyboard navigation - Demo assets for dubbing, dictation and voice design, plus the scripts that render them - Backend: validation-error handling, ASR request-path degradation, and the accompanying tests - CHANGELOG entries for the above and for the Wayland dictation fix Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(tests): follow Engines to the Model Catalogue, and green the sweep - test_supertonic3 asserted the license gate points at "Settings" while the engine now names Model Catalogue → Engines, which is where the accept button actually lives. The assertion follows the move; what it pins is unchanged — the hint must name a place the user can reach it. - Carries the CJK allowlist entries for the rendered dub bundle (#1517) and the regenerated route snapshot for /workers/agent (#1516), both of which this branch inherits from the workspace sync. - docs/install/linux.md: the dictation capsule is bottom-anchored everywhere except Wayland, where the protocol gives applications no say in their placement. Documented rather than left as a surprise (CodeRabbit). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: stop a flaky dependency fetch from failing green runs en-core-web-sm resolves to a direct GitHub release URL, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own three retries all land within the same few seconds and fail together, so the whole job dies on a dependency that has nothing to do with the change under test — it cost #1518 and #1517 an otherwise-green run tonight. Two changes: back off between whole `uv sync` attempts, which is what actually clears it, and pass --no-sync to the pytest steps. `uv run` re-resolves the environment before running, so every test step was a fresh chance to hit the same fetch even though the install step had already synced — that is exactly how #1518 failed, in the isolated backend/tests step, with all 5467 tests already passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: one retry seam for every uv sync, not just the job that failed last en-core-web-sm resolves to a direct GitHub *release* URL rather than a package index, and github.com intermittently answers `http2 error: refused stream before processing any application logic`. uv's own retries all land inside the same ~10 seconds and fail together, so a job dies on a dependency unrelated to the change under test. Tonight that cost four otherwise-green runs across #1515, #1517 and #1518 — and the first fix only covered the Tests job, so the next failure simply moved to Smoke (Linux), which syncs separately. The fetch is per-job, so the fix has to be per-job: scripts/uv-sync-retry.sh backs off between whole attempts (15s, 45s, 90s) and every workflow that syncs now goes through it — ci.yml (tests + the platform matrix), release.yml, security.yml, evals.yml. It still fails loudly after four attempts, so a genuinely broken lockfile is not disguised as a flake. The Tests job also lacked the UV_HTTP_TIMEOUT / UV_HTTP_RETRIES the smoke matrix has always set, which is part of why it was the one that kept dying; it has them now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(ci): pin the Intel-Mac contract by intent, not by command spelling test_ci_verifies_intel_mac_as_the_documented_remote_only_host asserted the literal line `run: uv sync --extra pockettts`, so routing every sync through scripts/uv-sync-retry.sh read as a broken Intel-Mac contract. The contract it exists to protect is that the pockettts extra installs ONLY on backend_supported legs — which the regex now pins, while leaving how the sync is invoked free to change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: keep every uv run out of the resolver, and bound the retry budget CodeRabbit, #1517: - `uv run` re-resolves before running, so the smoke suite, the worker-artifact tests, the release test run and the eval run were each a fresh chance to hit the flaky direct-URL fetch outside the retry loop. All of them pass --no-sync now; the environment is already synced by the step that owns the retries. security.yml's `uv run --with pip-audit` is deliberately left alone — it layers an ephemeral package rather than running the project's own tests. - The retry count multiplied uv's own budget (UV_HTTP_RETRIES=5 with a 120 s timeout on the smoke matrix). Three attempts and 60 s of total backoff outlast the refusals actually observed while staying well inside the jobs' timeout-minutes. - The Intel-Mac contract test pinned the smoke command literally too, so --no-sync tripped it exactly like the sync line did. Same fix: assert the contract (smoke runs only on backend_supported legs), not its spelling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
40662aeb06 | fix: close log safety review gaps | ||
|
|
72ce9b79f2 |
Merge remote-tracking branch 'origin/main' into fix/ghas-log-safety
# Conflicts: # CHANGELOG.md # backend/api/routers/batch.py |
||
|
|
77be03b130 | fix(security): fail closed on cleanup and redaction errors | ||
|
|
854a637b83 | fix(security): make untrusted log values single-line | ||
|
|
5cab8e0149 |
feat: rename the product to VoiceStudio (previously OmniVoice-Studio)
Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.
Deliberately NOT renamed, because renaming any of them silently breaks
an existing install — there is no legacy-path fallback anywhere in this
codebase:
- bundle identifier com.debpalash.omnivoice-studio (MSI UpgradeCode,
macOS TCC grants, managed venv, WebView localStorage, the
single-instance lock)
- data directories OmniVoice / .omnivoice and omnivoice.db
- the ~150 OMNIVOICE_* environment variables
- the X-OmniVoice-* HTTP headers (a wire protocol)
- the published Docker image paths
- the OmniVoice ENGINE, which is a model name and not this product
tests/test_identity_paths_survive_the_rename.py pins every one of those
so a future well-meaning sweep cannot orphan a user's library.
Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
|
||
|
|
fc7fbf1227 |
fix(gpu-pool): bound execution, not queue wait (#1190, #1202)
A job queued behind a busy 1-worker pool burned its whole 300s budget without executing an instruction, then reported "too heavy for the available compute". The clock now starts when a worker picks the job up; queue wait has its own generous bound and surfaces as a retryable saturation error. Also: reset() no longer cancels innocent queued peers; the timeout message stops claiming capacity was restored (the abandoned job keeps the device until it drains); every GPU dispatch uses the shared length-scaled budget; watermark embeds move off the GPU pool; /v1/audio/speech gets 429/503 + Retry-After; a timed-out batch segment fails the job instead of shipping a silent gap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
63fd497caf |
feat: TTS-only first run, platform-curated ASR, guided OS permissions, parakeet-mlx
Only the TTS model (~2.4 GB) is required on first run; ASR models are per-platform curated picks (curated_on in models.yaml) installed on demand. Every transcription surface returns a typed asr_model_missing error with a one-click download CTA instead of silently pulling multi-GB Whisper weights. Settings -> Models is a grouped, platform-aware catalog. New guided permissions UX (wizard System Check + Settings -> Permissions + mic pre-flight) with native mic-state checks and OS settings deep-links. New parakeet-mlx engine brings Parakeet TDT v3 to Apple Silicon (language-gated capture preference so multilingual dictation never regresses). Docs: expressive-speech page, Flush/Unload + CPU-fallback triage, clone-length FAQ. Hardening: preflight fails open for custom model pins, ROCm curation no longer inherits NVIDIA picks, Windows mic probe reads the NonPackaged consent key, CaptureWidget setup race fixed, offline-cache CI simulation fixes so empty-cache runners stay green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
3a7368cb26 |
fix(watermark): route every synthetic-audio producer through one mark_synthetic chokepoint (#1169)
POST /v1/audio/speech returned synthetic audio without the AudioSeal provenance watermark while /generate marked the same text — and the audit behind #1169 found the same class of gap in five more producers. EU AI Act Art. 50(2) (applicable 2026-08-02, expressly carved out of the open-source exemption in Art. 2(12)) makes machine-readable marking of synthetic audio a provider obligation, so per-door coverage gaps are compliance bugs. Root cause: coverage grew call-site-by-call-site (three separate embed_watermark calls) with nothing forcing a new producer to opt in. The fix, per the whole-class rule: * services/watermark.py grows `mark_synthetic(wav, sr, *, context, force=False)` — THE named chokepoint (delegates to embed_watermark: pref-gated, no-op without AudioSeal, never raises / degrades to unmarked) — plus `will_mark()` for cache-key derivation. * Gaps closed at the tensor stage, before any encoding: - openai_compat `_run_tts` (the reported gap; all response_formats) - tts_stream /ws/tts (per-sentence, before PCM16 framing) - generation stream=true preview chunks (marked streamed copy; the saved take keeps its single whole-take mark in finalize) - batch dub pipeline (assembled track, before WAV write / aac mux) - longform chapter render — covers /audiobook, /longform/render (Stories), /audiobook/preview and /audiobook/resume; the chapter cache key now carries a watermark tag so stale unmarked cache entries can never be served for a marked-on render - dub preview-segment (docstring had declared it exempt) - archetype render (served preview + materialized profile reference) * Already-covered paths (generate finalize, dub segments, persona bundles) migrated onto the same chokepoint. * Documented non-producers/gaps instead of fake coverage: /stories/encode is a pure transcoder of user uploads (must NOT mark); the opt-in SoniTranslate sidecar synthesizes+muxes externally and is a documented provenance gap at its route. * Regression tests: tests/test_synthetic_audio_watermark_1169.py runs detect_watermark() on the actual response audio of every producing route (real watermark service, fake AudioSeal nets, fake engine; verified fail-before on the pre-fix tree — 11 of 13 fail). * Recurrence-proofing: tests/test_watermark_route_coverage.py structurally asserts every synthesis call site references mark_synthetic (justified allowlist), producers keep their call, and embed_watermark is never called outside the chokepoint. Settings toggle semantics are unchanged (the issue's two legal questions stay flagged for a lawyer, deliberately unanswered here). |
||
|
|
a7efaa24f7 |
fix(dub): background bed no longer plays quiet and muffled — cancel amix normalization, mix at 48 kHz (#1136)
* fix(dub): background bed no longer plays quiet and muffled — cancel amix normalization, mix at 48 kHz Reported live: "background music is so much not like the original." Two stacked fidelity bugs in every bed-mix site, measured with real ffmpeg on a real dub job: 1. LEVEL — ffmpeg's amix NORMALIZES its inputs, so the per-site weight strings meant "favor dialogue slightly" but actually played the music bed at ~57% of its original level (batch.py stacked an explicit volume=0.15 under the same normalization, leaving its bed near 8%). 2. BANDWIDTH — the voice track is synthesized at 24 kHz and amix negotiates one common rate, so the 44.1 kHz bed was silently downsampled to 24 kHz: everything above 12 kHz (cymbals, air, brightness) vanished. Six call sites carried six hand-rolled variants of the same filter string (dub_export x5, batch x1) with inconsistent input ordering — the same copy-divergence pattern that orphaned the clone-prompt cache (#1130). They now share one builder, services.ffmpeg_utils.bed_mix_filter(): both inputs resampled to 48 kHz before the mix, a compensating volume multiply that cancels amix's normalization exactly (the weights ARE the absolute gains: bed 0.9, voice 1.1), and a transparent peak limiter for the rare summed peak that full-scale mixing makes possible. Measured A/B on the reporting user's job (bed vs bed-through-mix, silent voice): 57% -> 90% of original level, 24 kHz -> 48 kHz output. The remaining -0.9 dB is deliberate dialogue headroom, one constant to change if policy shifts. Tests: the export command must carry the resample + compensation + limiter (fails on the old strings), builder label-uniqueness for multi-track graphs, and the existing export suites unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(dub): amix renormalizes when a stream ends — disable normalization instead of compensating for it Greptile P1 on this PR, confirmed real by measurement: amix's normalization is DYNAMIC — it rescales the remaining inputs whenever one ends. The previous commit cancelled it with a constant post-mix multiply, which is exact only while both streams are active; once the (even marginally shorter) voice track ends, the bed's internal scale jumps to 1.0 and the fixed multiply BOOSTS the tail music into the limiter. Measured on the real job with a deliberately short voice: bed at 90% while the voice runs, 189% after it ends. The original A/B used equal-length streams, which is why this never showed. Fix: amix normalize=0 (a plain sum) with per-input volume gains — levels are exact for the whole timeline regardless of stream lifetimes. Same measurement now: 90% / 90%. normalize= arrived in ffmpeg 5.x, and system-ffmpeg users can be older, where an unknown option rejects the whole graph (= no export at all). The builder probes `ffmpeg -h filter=amix` once per process and falls back to the compensated form on legacy builds — its tail quirk is the lesser evil next to a failed export, and every bundled/imageio tier ships 7.x. Tests: both paths pinned (normalize=0 + per-input gains on modern; the compensation multiply on legacy), probe monkeypatched per test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(dub): anchor the amix monkeypatches to the call chain — module aliases miss under random order Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a99f1fdff9 |
feat(tts): normalization covers the OpenAI-compat API, streaming, and batch paths (#1054)
The engine-agnostic text-normalization pre-pass now runs at the three remaining text→engine choke points, applied exactly once per request: /v1/audio/speech (req.language), /ws/tts (whole text, before the sentence chunker fans it out), and the batch queue's per-segment _gen (target language) — matching the /generate, dub, and audiobook wiring. Route-level tests pin exactly-once (spy) + toggle-off-raw for each path. Also fixes a pre-existing /ws/tts bug the new test exposed: any request omitting emo_alpha hit a KeyError and got an error frame instead of audio. 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>
|
||
|
|
e347f99542 |
fix(tts): bound + reset the GPU pool on a hung generate so it can't brick the backend (#730 class) (#851)
* fix(tts): bound + reset the GPU pool on a hung generate so it can't brick the backend (#730 class) A GPU job that wedges on some Windows+CUDA setups occupies its worker forever — run_in_executor can't cancel the thread — so on the 1–2 worker pools we ship, one stuck job starves every other request and the next action surfaces as the misleading "Can't reach the local backend" even though the process is alive. ASR/dub/model-load already bound+reset the pool on hang (#730). The TTS **generate** paths (generation.py, tts_stream.py) were the last unguarded GPU dispatch — and the residual on-main reports (#850 #802 #755 #723 #721, plus the 0.3.7 generate cohort) all fail on generate:start (audio). - model_manager: add run_on_gpu_pool_guarded() + GpuJobTimeoutError, a generalized version of the ASR guard so every GPU dispatch shares one bound+reset recovery path. Env-tunable via OMNIVOICE_GENERATE_TIMEOUT_S (default 300s). - generation.py: route both inference branches + the reference-clip transcribe through the guard; map a timeout to an actionable 503. - tts_stream.py: same guard on the streaming path (timeout → error frame). - test_generate_timeout_730: fail-before/pass-after regression (timeout resets pool + restores capacity, happy path, env override, no-reset exec). - docs + CHANGELOG: extend troubleshooting §14 to cover generate; document the new env var. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tts): extend the GPU-pool hang guard to batch/dub/archetype/openai-compat generate (#730 class) The generate-hang class wasn't only in Studio + streaming: batch generate, the dub per-segment + preview generate, archetype preview render, and the OpenAI-compat /v1/audio/speech path all dispatched the TTS model to the GPU pool with no wall-clock bound either. Any one of them wedging on a Windows+CUDA hang starves the pool and bricks the backend the same way. Route all of them through run_on_gpu_pool_guarded so the whole class is closed — a hung generate anywhere resets the pool and returns an actionable timeout instead of a dead backend. Batch/dub recover per-segment on a fresh worker; drop the now-dead loop/_gpu_pool/asyncio locals ruff flagged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0fc9f2afec |
fix(asr): bound every transcribe path + reset the GPU pool on hang so a wedged ASR can't brick the backend (#730) (#731)
A whisperx/CTranslate2 transcribe can hang hard on some Windows+CUDA setups and never return. ASR shares the small (1-2 worker) _gpu_pool with TTS, so one stuck worker starved every other request — the next TTS generate then surfaced as "Can't reach the local backend" though the process was alive (#720/#721/#723). Two parts: - Bound the three remaining unguarded whole-file transcribe paths (dub whole-file dub_core.py, batch.py, live-dictation capture_ws.py) with run_transcribe_guarded, matching the dub-QC/dictation/OpenAI paths that were already bounded by #656. - On timeout, run_transcribe_guarded now calls executor.reset() when the pool supports it (_ResilientGpuPool, already built for the model-load-timeout case in #589/#599): the wedged worker is abandoned and the next submit gets a fresh one, restoring capacity without an app restart. Best-effort — a plain ThreadPoolExecutor (tests) just gets the bound + actionable error. Regression tests: pool.reset() is invoked on timeout; a non-reset pool still bounds cleanly. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5d602c8871 |
fix(tts): let studio engines skip the broadcast mastering chain (#311)
* fix(tts): let studio engines skip the broadcast mastering chain apply_mastering() (HighpassFilter + Compressor + 8% Reverb) is tuned for OmniVoice's 24 kHz clone output. The OpenAI-compatible /v1/audio/speech route (_run_tts) runs it on every engine, including VoxCPM2 — whose native 48 kHz output is already studio-grade. There the compressor pump and the reverb tail are audible degradation rather than polish. Add an opt-out class flag TTSBackend.applies_own_mastering (default False, so all existing engines are unchanged) and set it True on VoxCPM2Backend. _run_tts() skips apply_mastering() when the active backend declares it. Loudness normalisation still runs for every engine (benign peak scale). * fix(tts): also skip mastering on the streaming route for studio engines tts_stream.py is the other route that runs the *active* TTS backend (get_active_tts_backend), so it needs the same applies_own_mastering guard as openai_compat._run_tts — otherwise VoxCPM2 output is still pumped/reverbed when streamed. The remaining apply_mastering() call sites (generation.py, batch.py, batched_tts.py, dub_generate.py) run the OmniVoice model directly via get_model(), never the active backend, so VoxCPM2 cannot reach them. * docs(tts): mark OmniVoice-only mastering sites with TODO(#312) Per review: instead of always-False guards on routes that never run the active backend, leave a pointer so the applies_own_mastering guard is added exactly when those routes become engine-aware (issue #312). |
||
|
|
47729057bd |
chore(lint): remove unused imports + variables (ruff F401/F841) (#210)
Autofixes the genuine lint behind the CodeQL py/unused-import and py/unused-local-variable note-level alerts — actually removing the dead code rather than dismissing it. 68 safe fixes via 'ruff check --select F401,F841 --fix' across 29 backend files (dead stdlib/symbol imports like io/sys/json/torch/typing.Optional and unused locals). Only ruff's safe fixes applied — the 9 'unsafe' fixes and the audio_dsp numpy availability import were left untouched. Not touched: empty-except (needs per-site judgement, not autofixable); frontend js/unused-local-variable (eslint no-unused-vars has no autofix); the loopback-low-risk path/log/stack-trace alerts (real, left visible). Verified: full tests/ suite unchanged at 601 passed (the 2 test_supertonic3 failures are pre-existing on main, local .venv state, green in CI). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b64f53b0af |
feat: pipeline error transparency — no more silent "unknown error" (plan-04, closes #131) (#136)
* docs(plan-04): spec + plan for pipeline error transparency (#131) speckit spec/plan/research/data-model/contract/quickstart for plan-04. Grounds the fix in the real code map: shared failure-event builder (backend/core/failure.py) feeding tasks.py + dub_pipeline.py + dub_core.py, non-empty reason guarantee, sanitized diagnostic block, frontend renderer with docs deeplink. Closes-target: #131 (children #122, #63). Design only — no code changes yet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(pipeline): structured, non-empty failure events + logged tracebacks (#131) plan-04 backend: no more silent "unknown error". A shared failure helper guarantees a non-empty reason at every emit site and a sanitized, copyable diagnostic block. - backend/core/failure.py: build_failure()/build_failure_event() (reason falls back to the exception class name), sanitize() (reuses the logging_filter HF-token regex + redacts *TOKEN*/*KEY*/*SECRET* env values + home→~), diagnostic() (reuses the env capture), classify() reusing the error_docs_map 5-class taxonomy for the docs deeplink + hint. - core/tasks.py worker: structured event instead of bare str(e); keeps the logged traceback. - services/dub_pipeline.py: enrich download/extract error yields; ADD the missing outer `except Exception` (the #122 path — unhandled ingest errors were never surfaced with stage context); surface the previously-silent demucs/scene/thumbnail degradations as non-fatal `warning` events. - api/routers/batch.py: guaranteed non-empty batch failure reason. SSE payload is additive (legacy `error`/`stage`/`detail` keys preserved), so existing frontends keep working and already show the specific reason. Tests (TDD, fail-before/pass-after): 14 cases — non-empty-reason guarantee, redaction, diagnostic sanitization, and the 3 Test-matrix triggers (worker / extract / url). 483 passed, 0 regressions. Closes #131. Refs #122, #63. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dub-ui): show specific cause + docs deeplink + copyable diagnostic (#131) plan-04 frontend. The backend now sends a structured, non-empty failure; surface it to the user instead of "extract: unknown error". - dubSlice: DubFailure type + dubFailure state/setter. - useDubWorkflow: capture the structured failure on the SSE error event (reason/error_class/stage/hint/docs_topic/diagnostic); clear on new runs. - DubTab: DubFailureNotice renders the actionable hint, an "Open docs" deeplink (via the existing errorDocsMap classifier), and a "Copy diagnostic" button — shown beneath the error badge in both failure banners. typecheck + build clean; 66 frontend tests pass. Refs #131, #122, #63. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(failure): annotate intentional best-effort excepts (CodeQL) The new security workflow's CodeQL flagged 5 bare `except: pass` blocks. All are deliberate best-effort guards (sanitize/diagnostic must never throw on the failure path; the test cancels the worker to tear it down). Added explanatory comments per CodeQL's py/empty-except rule. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c6e9bbc191 |
Phase 2 Plan 02-02: audio I/O hardening + WAV-export correctness (#96)
* Phase 2 02-02: add _safe_torchaudio_save + _safe_soundfile_write helpers
Centralizes WAV/audio writes through a single audited path that defends
against the four documented torchaudio.save failure modes (CUDA/MPS
tensor, non-contiguous, out-of-range, wrong dtype) AND the torchaudio
2.9+ TorchCodec-delegation behavior drift.
* services/audio_io.py:_safe_torchaudio_save now performs:
- .cpu() move (torchaudio cannot serialize CUDA/MPS)
- dtype coercion to torch.float32
- .clamp(-1.0, 1.0) (out-of-range = silent clipping on some backends)
- .unsqueeze(0) for 1D (mono) inputs
- .contiguous() (torch.cat of slices = non-contig = silent corruption)
- explicit encoding="PCM_S/PCM_F" + bits_per_sample so future
torchaudio backend selection cannot drift the on-disk format
- format passthrough for wav/flac/mp3/ogg with encoding-kwarg fallback
for older codec builds
* services/audio_io.py:_safe_soundfile_write — sibling helper for the
one sf.write call site (dub_core.py). Applies the same dtype/contig/
range checks before delegating to soundfile.write.
* services/audio_io.py:atomic_save_wav (existing P0 helper) now
delegates the actual encode to _safe_torchaudio_save so atomicity
and correctness compose: every byte that lands at the target path
was produced by the audited helper.
* tests/backend/services/test_audio_io.py — 29 tests (25 pass + 4
skipped for MPS dtype incompatibility): parametric round-trip across
dtype x device x contiguity, plus out-of-range clamp, format
passthrough, in-memory buffer, empty-tensor rejection, 1D auto-
unsqueeze, and a smoke check that atomic_save_wav inherits the
safety guarantees.
No new Python dependencies. SoniTranslate untouched (D1 locked).
Refs BUG-01 / #48.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 2 02-02: migrate router audio writes through audited helpers
Migrates all 12 grep-audit bare audio-write call sites in
backend/api/routers/ to route through services.audio_io. Closes the
last surface area of BUG-01 / #48 that the P0 atomic-write commit
(
|
||
|
|
a1ef66c321 |
Stability pass: DB leaks, App.jsx hooks refactor, desktop bootstrap (#49)
* fix: eliminate DB connection leaks, race conditions, and deprecated asyncio API ## DB Connection Leaks (P0) - Convert 38 raw get_db() calls to db_conn() context manager across 14 router files - Connections are now guaranteed to close even when exceptions are raised - profiles.py create_profile: clean up orphaned audio file if DB insert fails - profiles.py lock_profile: consolidate 3 separate conn.close() error paths ## Race Condition (P1) - Add _dub_jobs_lock (threading.Lock) to protect _dub_jobs dict in dub_pipeline.py - get_job/put_job now thread-safe for concurrent dub sessions ## asyncio Deprecation (P2) - Replace 23 asyncio.get_event_loop() calls with asyncio.get_running_loop() - Prevents DeprecationWarning on Python 3.12+ and future breakage on 3.14 ## Quick Fixes - gallery.py preview_voice: remove filesystem path from error response (P2) - dub_pipeline.py parse_vtt_segments: remove redundant `import re` inside loop (P3) - gallery.py _init_gallery_db: use db_conn() context manager (P2) * refactor: extract hooks, centralize isTauri, add pytest-cov ## Frontend - Extract useTTS hook (150 LOC) — TTS generation, streaming, audio ingestion - Extract useProfiles hook (219 LOC) — voice profile CRUD, lock/unlock, preview - Centralize isTauri detection: dialog.js, VoiceGallery.jsx, Settings.jsx now import from utils/media.js instead of 4 different detection patterns ## Backend - Add pytest-cov to dev dependencies - Baseline coverage: 39% across backend/ (214 tests pass) - Add .coverage to .gitignore * feat: add Vitest + checkJs, extract useDubWorkflow + useAppData hooks ## Frontend Testing (new) - Set up Vitest with jsdom environment + @testing-library/react - 11 tests: utils (isTauri, formatTime, constants) + Zustand store (mode, text, dubStep, pill) - Scripts: 'test' (vitest run), 'test:watch' (vitest), 'test:legacy' (node runner) ## App.jsx Decomposition (continued) - Extract useDubWorkflow hook (387 LOC) — upload, ingest, transcribe SSE, translate, generate SSE, abort, stop, cleanup - Extract useAppData hook (181 LOC) — data loading, localStorage persistence, WebSocket real-time updates, model-status pill management ## TypeScript checkJs - Enable checkJs: true in tsconfig.json for IDE-level type checking - 947 existing errors (informational, not blocking builds) - noImplicitAny remains false to avoid blocking * ci: add Vitest step, fix useProfiles duplicate state ## CI - Add 'Run Vitest (frontend)' step — runs 11 unit tests - Override --checkJs false in CI typecheck to avoid 947 pre-existing errors - Rename legacy test step for clarity ## Hooks - Fix useProfiles to accept loadProfiles from parent (useAppData) instead of managing its own duplicate profiles array * refactor: wire hooks into App.jsx — 2067 → 1129 LOC (-45%) App.jsx now delegates to extracted hooks instead of inline logic: - useAppData: data loading, localStorage, WebSocket, model pill - useProfiles: voice profile CRUD, lock/unlock, preview - useTTS: generation, streaming, audio ingestion - useDubWorkflow: upload, transcribe SSE, translate, generate SSE 988 lines removed. All handler logic lives in focused, independently testable hooks. Store selectors and render JSX stay in App.jsx as the shell. Verified: vite build clean, 11 frontend + 214 backend tests pass. * feat: show real-time percentage on model loading pill Backend: register hf_progress listener during _load_model_sync() so download/weight-loading tqdm events update _loading_detail with a progress percentage (0-99%). get_model_status() now includes a 'progress' field that the frontend polls. Frontend: useAppData reads msQuery.data.progress and calls setPillProgress() — the FloatingPill already renders the percentage text and progress bar width from this value. * fix: prevent FileNotFoundError in desktop bundle during model init transformers >=4.52 calls _can_set_experts_implementation() and _can_set_attn_implementation() during PreTrainedModel.__init__, which open the class source file via open(class_file). In a Tauri desktop bundle, module.__file__ points to a path that doesn't exist on disk, causing: FileNotFoundError: .../omnivoice/models/omnivoice.py Override both classmethods on OmniVoice to return static values without filesystem access. OmniVoice doesn't use MoE experts (return False), but does support flex/flash attn (return True). * fix: sync source dirs on every bootstrap, not just first run The Tauri bootstrap previously only copied omnivoice/ and backend/ to Application Support on the first run. Subsequent app updates kept using stale source files, preventing bug fixes from landing. Now ensure_venv_ready() always syncs both directories from the bundle resources before returning, even when the venv is healthy. This fixes the FileNotFoundError crash where the old omnivoice.py lacked the _can_set_experts_implementation override. * ui: premium setup wizard polish - Primary button: solid gradient fill with hover glow + lift + press - Stepper nav: connected pills with glow ring on active step - Welcome cards: glassmorphism with stagger-in animations, lucide icons, left-border accent strip, hover translate - Preflight panel: colored icon pill backgrounds, stagger-slide entrance - Step transitions: fade+slide animation via keyed wrapper - Footnote: shortened paths (~/ notation), Reveal in Finder button - Recommendation banner: gradient background with accent glow - Compact spacing throughout for denser, professional layout * fix: kill zombie backend on clean+retry bootstrap When clean_and_retry_bootstrap removes the project dir, any old uvicorn process still running from the deleted paths remains alive on port 3900. The subsequent retry_bootstrap sees the port is healthy and attaches to the zombie instead of re-bootstrapping. Now explicitly kill any process on the backend port after cleaning, before calling retry_bootstrap. * feat: integrate speaker clones into dubbing interface, sanitize system environment variables for subprocesses, and improve FFMPEG binary path resolution. * fix: restore docker compose default + drop dead setSeed call - deploy/docker-compose.yml: remove profiles: ["cpu"] from the default service so `docker compose up` matches the comment on line 5. With the profile present, no service auto-started. - frontend/src/App.jsx: drop the setSeed call in restoreHistory. The selector was never reintroduced after the App.jsx hooks split, and there is no seed state in the store — seeds are generated fresh per call in useTTS and only read from history items for display. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address CodeRabbit review — async detection, dub stream, bootstrap fail-fast - backend/services/tts_backend.py: invert async-context detection in _ensure_loaded. The previous code unconditionally caught its own diagnostic RuntimeError and then called asyncio.run() inside a running loop, masking the intended error message. - frontend/src/hooks/useDubWorkflow.js: require a terminal `done` event before reporting dub success. Without this, a dropped stream after partial progress would flip the UI to `done`, refresh history, and play the completion ping as if generation finished. - frontend/src/hooks/useDubWorkflow.js: restore the previous step when tasksCancel() fails. The UI was getting stuck in `stopping` forever on cancel errors. - frontend/src-tauri/src/bootstrap.rs: fail-fast when source sync fails after the existing directory has already been removed. The previous warn-and-continue path could leave the install with no backend/ or omnivoice/ sources and defer the failure to backend startup with a cryptic error. - backend/api/routers/generation.py: add `from e` to the ValueError → HTTPException re-raise (Ruff B904). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: preserve % suffix in TTS generation timer The 100ms timer in useTTS was rewriting generationTime to a plain elapsed-seconds string, which immediately wiped the "(xx%)" download suffix written on the next iteration of the response-body loop. The real-time percentage was flickering on/off as a result. Read the previous value inside the setter and reattach any existing percent suffix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8d11e19494 |
feat: dictation maturity + batch TTS pipeline + tests (#32)
Global Hotkey:
- Register ⌘+⇧+Space system-wide via tauri-plugin-global-shortcut
- Shows/focuses window and emits tray-dictate event from any app
Auto-Paste:
- enigo crate simulates ⌘V/Ctrl+V after transcription
- Text auto-pastes into whatever app was active before dictation
Streaming ASR:
- WebSocket endpoint /ws/transcribe for live partial transcription
- 2s buffer interval, configurable via OMNIVOICE_STREAM_INTERVAL
- CaptureButton streams audio chunks, shows italic partial text
- Falls back to HTTP POST if WebSocket unavailable
Batch TTS Pipeline:
- Replace stub worker with full pipeline:
extract → transcribe → translate → generate → mix → export
- Per-job progress tracking (stage, percent, current_lang, segment)
- GoogleTranslator integration via deep_translator
- Download endpoint GET /batch/download/{id}/{lang}
- BatchQueue UI rewritten: progress bars, cancel/delete, downloads
- Type-safe API client (api/batch.ts)
Tests:
- 23 tests for batch endpoints + streaming ASR helpers
- Lightweight fixtures that stub GPU deps
UX (earlier sessions):
- Dual-mode ASR (Turbo MLX + WhisperX Accurate)
- Enhanced download progress (speed, ETA, bytes)
- Status bar black flash fix
- Cold-start model preloading
- Full accessibility audit (ARIA, focus-visible)
- Compact UI layout improvements
- README updated with new features
|
||
|
|
fc76e79ff8 |
feat: setup wizard, donate page, CI fixes, performance optimizations, and style extraction
- Implement donate page and migrate API fetching to react-query hooks - Add setup wizard for batch job management and voice clip editing - Refactor setup router into package (wizard, models, download sub-modules) - Fix 9 CI test failures from setup router refactor - Fix cross-device link error in prefs.py atomic writes - Fix event loop mismatch in export test fixtures - Modernize README with architecture diagram and 13 app screenshots - Defer per-segment disk writes in dub_generate for ~6s faster dubs - Extract 45 inline styles from Launchpad, KeyboardCheatsheet, DubSegmentRow - Add playwright dev dep and screenshot capture script |