d91beef0fd314250d8d9b94de86dfea019a8bd96
37
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3d0c9605df |
test(shell): backend-lifecycle fault-injection harness (#1551)
* test(shell): backend-lifecycle fault-injection harness Runs spawn_backend_and_wait/supervise_backend against REAL dying child processes and asserts the user receives the correct NAMED diagnosis — not merely that recovery happens. Wrong/missing explanation was 61% of the historical "can't reach the backend" class; this rig is the permanent regression harness for every future lifecycle fix. Seam: OMNIVOICE_BACKEND_CMD (JSON argv or whitespace form) runs any command as "the backend" — venv bootstrap and ffmpeg resolution are skipped, everything else (err-log run offsets, drainer threads, env pinning, real OS pipes, spawn-failure diagnostics) stays real. Plus OMNIVOICE_LOG_DIR (per-test log+marker dirs, also a support tool) and harness-only timing overrides OMNIVOICE_STARTUP_BUDGET_S / OMNIVOICE_SUPERVISOR_POLL_MS whose production defaults are pinned by unit tests. Lifecycle fns genericized over tauri::Runtime for the MockRuntime app; behavior-neutral with the env unset (unit-pinned). Scenarios (tests/backend_lifecycle.rs, scenario children = this test binary re-invoking itself; serial by mutex + CI --test-threads=1): - port conflict (exit 78) → the detectHints-matchable port phrasing - generic chained traceback → root cause survives into the diagnosis and the crash marker - spawn failure → spawn diagnostic reaches the user, NO bogus marker - slow start past budget → timeout names the budget + last stderr - post-Ready crash loop → 3 restarts announced, markers before restarts, "kept crashing" diagnosis naming the last exit - SIGKILL (unix) → named as signal 9 - deliberate kill → supervisor yields silently, no marker, never Failed - deferred-startup FATAL → the named step reaches the user, forensics, and the splash narration CI: harness added to the 3-OS tauri-cross-platform matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(ci): temporary Windows loader bisect probe for the harness binary Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(shell): embed Common-Controls v6 manifest into Windows test binaries Bisected on #1551: EVERY integration-test binary of this crate died at load on Windows with STATUS_ENTRYPOINT_NOT_FOUND (0xc0000139) — cargo gives test binaries no manifest, so the loader resolves comctl32 v5, which lacks the TaskDialogIndirect entry point tauri's dialog/tray stack imports. build.rs now embeds tests/windows-test.manifest via rustc-link-arg-tests on Windows targets. Bisect probe removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(test): scenario gate is PID-valued — the parent can't self-inject CodeRabbit on #1551: in a parallel local `cargo test`, the parent's own scenario_child test could observe the armed env and start playing the backend in-process (binding the port, idling 600s). The gate value is now the arming process's PID; a matching PID stays inert, so only the spawned child — a different process — runs the scenario. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0ee62b2261 |
feat(gallery): save gallery voices as profiles, with validated audio references (#1542)
* feat(gallery): save gallery voices as profiles, with validated audio references Work-in-progress lifted from the concurrent gallery session at the owner's request (its uncommitted working tree, preserved verbatim from base 92b1ee5d; safety snapshot remains at rescue/gallery-wip): - gallery voices can be saved as local profiles: audio is copied into the profile store with content-addressed filenames, existing profiles are detected and refreshed only when the source clip changed - backend/core/audio_validation.py: symlink-rejecting, root-contained resolution for persisted profile WAV references, with tests - archetype/community routers and the Voice Gallery UI updated for the save-as-profile handoff (spec: docs/specs/longform/26-gallery-use-handoff.md) - locale updates for the new gallery strings across all 21 files Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore: drop a stray local screenshot script that rode in with the tree copy * fix(community): explain the tolerated Content-Length parse failure; drop an unused import CodeQL on #1542: the empty except now says why it is safe (the streamed byte counter enforces the same cap regardless), and the test file loses an unused Path import. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(gallery): review findings — copy outside the write lock, no stale completions CodeRabbit on #1542, all findings addressed: - the profile-audio copy stages to a .part temp BEFORE BEGIN IMMEDIATE and publishes via atomic os.replace inside it — other backend writers no longer block for the duration of an audio copy; a mid-copy failure leaves no temp droppings and no profile row (both pinned by tests) - VoiceGallery async ops carry per-operation generation tokens: a preview or save-as-profile that resolves after unmount (or after a newer operation) can no longer play audio, redirect into a workspace, or touch state — three fail-before regression tests - VoiceGalleryActions imports the page at test runtime; the e2e locator uses a stable data-testid instead of a translated string; symlink tests skip cleanly where the OS can't create symlinks; the changelog line carries its PR ref Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: static ffmpeg fallback when the chocolatey feed is down Third feed outage to break a PR run (2026-07-20, 2026-07-28, today — three attempts, three 'installed 0/1'). Chocolatey is a distribution channel, not the dependency: after the retry loop exhausts, fetch the static gyan.dev build from its GitHub release mirror and put it on PATH — same binary, no feed in the path. URL verified live (HTTP 200). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
41c098e009 |
feat(demos): ship the demo audio and video the app already advertises (#1517)
* feat(demos): ship the demo audio and video the app already advertises Every demo asset in the app was a dead link on anything but a Mac. `personalities.py` has carried a `preview_url` for each of the seven voice-design presets since they were added; DictationDemo.jsx posts three bundled WAVs to /transcribe so the feature can be shown without microphone permission; the Dub workspace reads a manifest and plays a source video plus four dubbed languages. None of those files were committed, because the tooling that renders them (scripts/build_demos.sh, scripts/build_dub_demo.sh) hard- requires macOS `say` — it even carries a `TODO: add espeak-ng path for Linux contributors`. So the presets returned 404, the replay buttons did nothing, and the dubbing demo never loaded. Rendered with VoiceStudio's own engine, which runs wherever the app does: - 7 voice-design previews (2.2 MB) - 3 dictation replay clips (1.1 MB) — verified by transcribing them back: the conversational and French clips round-trip exactly - dubbing demo: source + 4 dubbed videos with subtitles and manifest (9.6 MB) Tooling fixes this turned up: - build_dub_demo.sh wrote to backend/assets/demo/dubbing, but main.py mounts backend/assets/samples at /demo_audio — so the frontend's /demo_audio/demo/dubbing/manifest.json could never have resolved even after a successful Mac build. Output moved under the mount. - `say` is now the fallback rather than the requirement: the new scripts/render_dub_demo_audio.py renders the five tracks with the engine and the shell script picks them up. - The five demo paragraphs lived in two files. They are now one JSON both read — two copies is one edit away from a video whose subtitles disagree with it. - render_demos_omnivoice.py peak-normalized, which a single-sample transient defeats: the Helpdesk preset landed at -30 dB RMS against -17 dB for its neighbours, so the preview row played at wildly different volumes. Now EBU R128 at -18 LUFS with a -1.5 dBTP ceiling. - …and pinning the output rate, because loudnorm resamples to 192 kHz internally and writes there unless told otherwise, which turned 2.1 MB of previews into 17.5 MB of identical-sounding audio. - update_manifest() looked for a manifest at a path nothing writes, so it always printed "not found" and did nothing. - Dictation is rendered here now too. It was excluded on the grounds that `say` was good enough and engine TTS was overkill — true only on macOS. tests/test_demo_assets_exist.py resolves every advertised URL against the directory main.py actually mounts, and checks each dubbing subtitle matches the script its manifest entry claims. A missing static file is not an import error and not a failing request; nothing would have caught this otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(changelog): stamp the demo-asset entries with their PR ref Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(demos): watermark rendered demo audio, and harden the render scripts Review findings on #1517: - Greptile P1: the renderers wrote engine output straight to disk, so a re-render shipped demo audio with no provenance mark. These clips play back to users as VoiceStudio output — they are synthetic audio leaving the app like any other, and now go through mark_synthetic (#1169), the one chokepoint every producing route uses. It runs on the file AFTER loudnorm, since loudnorm re-encodes what it is handed, and says so loudly when marking is unavailable rather than committing an unmarked asset. The dubbing renderer shares the same helper. - CodeRabbit: build_dub_demo.sh checked only source.src.wav before deciding it could run without macOS `say`, so a Linux or Windows run with four of five tracks present reached a missing one, called `say`, and left a half-built bundle. It now requires all five. - CodeRabbit: shutil.move over an existing path delegates to os.rename, which raises FileExistsError on Windows — os.replace overwrites atomically everywhere. - CodeRabbit: the preview test discovered presets in a parametrize argument, importing app code at collection time and leaving core.personalities in sys.modules for later tests. Discovery moved into the test body. CI: the rendered dub bundle's zh/ja subtitles, its manifest and the script source are dubbing CONTENT, not UI strings — allowlisted in test_no_hardcoded_cjk.py with that justification. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(demos): a render that cannot be watermarked fails instead of warning CodeRabbit and Greptile, #1517: mark_synthetic degrades rather than raising — correct for generation, wrong for a render script, whose whole job is to produce files a human then commits. A printed warning on a scrolling console is not a gate, so both scripts exited 0 with unmarked assets sitting on disk ready to commit. They now raise, with the reason and the fix; OMNIVOICE_DEMO_ALLOW_UNMARKED=1 stays for a local listen. 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> |
||
|
|
64973d1829 | fix(release): validate wrapped AppImage launcher | ||
|
|
aa1d739843 |
feat(workers): dubbing goes remote, and the protocol stops lying to old workers
The remote-GPU line, verified on hardware rather than asserted. **Dubbing renders on the worker.** dub_generate.py dispatches the coarse `dub_segments` operation through the gateway, following the audiobook pattern: per-unit local fallback after consecutive remote failures, one aggregated notice rather than one per segment. A 40-minute dub that loses its worker at segment 200 degrades instead of producing 200 error rows. **An out-of-date worker is now refused by name.** This was the worst defect in the plan and it was silent: an un-upgraded worker registered cleanly, then ignored `inputs` and rendered a clone with NO reference audio — returned as success. A plausible wrong result with nothing anywhere to surface it. Workers now declare features, and one missing them is turned away with the features named and `no task was run`. Verified live: a worker one commit behind was correctly refused. **"Offline" and "cannot run this" are different facts.** Asking a live worker for an engine it lacks answered "is offline or cannot be reached. Wake the selected worker" — while that worker reported ready, one free slot and 3.6 ms latency. The user was sent to wake a machine that was already awake. The scheduler now distinguishes absent from present-but- incapable, and names the engine rather than the operation, because the engine is the thing a user can install. **An engine with no catalog entry is no longer hidden.** A `repo_ids` non-emptiness check had been implemented as a runtime filter, so a worker silently refused to advertise any engine lacking a models.yaml entry — which is four registered engines, including CosyVoice. Users with those already installed would have lost remote support with only a log line. Empty `repo_ids` now means "not downloadable here", never "not runnable". **And a script so this stops being done by hand.** scripts/verify-remote-worker.sh runs the per-phase acceptance checks against a live worker, non-destructively. Its preconditions are the mistakes that cost the most time: exactly one listener on the control port (two instances silently shared it), and never detecting the worker with a pgrep pattern that matches the ssh shell running it. Its first real run found the dubbing picker claiming remote placement. That turned out to be the CHECK being stale, not the picker — the port had landed since it was written. It now asserts self-consistency instead: the picker may claim remote only for an operation the control plane actually advertises as remotely producible, which cannot rot the next time an op is ported. Backend 5291 passed, frontend 1812 passed. Acceptance script: no automated failures across Phases 4-8 on an RTX 4090. Four checks remain MANUAL by design — true airplane mode, concurrent downloads, killing a worker mid-audiobook, and the model-list UI — and are reported as unverified rather than passed. |
||
|
|
9f2c8ac0a4 | fix(pockettts): close license review findings | ||
|
|
c1793dac41 | Merge remote-tracking branch 'origin/main' into codex/pr1442 | ||
|
|
5be4a26903 | fix(appimage): ship the compatibility launcher | ||
|
|
c53f1a8d1c | fix(ci): keep Intel Mac smoke contract honest | ||
|
|
9a770b15d1 | test(pockettts): verify pinned install on four platforms | ||
|
|
15afc6611d |
fix(linux): AppImage blank window on Mesa 26.1+ hosts (#1258, #1244) (#1265)
* fix(linux): AppImage blank window on Mesa 26.1+ hosts (#1258, #1244) The AppImage bundles an Ubuntu-built WebKitGTK but ships no libEGL, so that bundled WebKit runs against the HOST's Mesa. On Mesa >= 26.1 it calls eglGetPlatformDisplay() in a way the newer driver rejects and the app dies before it paints: Could not create default EGL display: EGL_BAD_PARAMETER. Aborting... No environment variable helps, because the failure is in EGL display creation — before WebKit consults any rendering-path flag. #1258 confirmed WEBKIT_DISABLE_DMABUF_RENDERER, WEBKIT_DMABUF_RENDERER_FORCE_SHM, WEBKIT_SKIA_ENABLE_CPU_RENDERING, EGL_PLATFORM=surfaceless and MESA_LOADER_DRIVER_OVERRIDE=swrast all fail identically. Chasing the build runner's WebKit (#961 bumped 22.04 -> 24.04) cannot fix this class: what we bundle is frozen and host Mesa keeps moving. So when the host has a WebKitGTK at least as new as ours, let it win — the bundle still fills every gap, and a host without WebKitGTK is untouched. That is exactly why building from source works on the hardware where the AppImage does not. The compositing workaround is re-decided against whichever library ends up running, and AppRun.test.sh — which had never been wired into CI — now runs there, so this logic stops being a regression test nothing executes. * fix(review): the ordering change was a no-op; name the host libdir explicitly CodeRabbit Major — correct, and it made the whole fix inert. LD_LIBRARY_PATH is searched AHEAD of the linker's default paths no matter where in that variable a directory sits, so on a normal launch (empty LD_LIBRARY_PATH) the bundle remained the only explicit search directory and still won. Merely appending it changed nothing. The host's WebKit libdir is now named explicitly, ahead of ours. The new tests fail 3/3 against the previous version. Greptile P1 — a host with the runtime but no -dev package has no .pc file, so pkg-config can't answer and the check rejected a perfectly good system WebKit. The libdir probe now falls back to ldconfig, and OMNIVOICE_PREFER_SYSTEM_WEBKIT gives those users an explicit opt-in (=0 opts out) rather than gambling on an unverified version, which would risk the #961 regression. CodeRabbit — my changelog script had also inserted the CI entry into the published 0.4.0 section. Removed; it belongs only under Unreleased. CodeRabbit — the docs' source-build fallback used 'cd frontend', not the repo-root flow the rest of the page documents. Fixed. |
||
|
|
d76c2ba675 |
ci: price every smoke leg for a cold uv sync, not a warm one
The Windows-only 25 came from a warm-cache measurement: Linux and macOS finish in ~65s when setup-uv restores its cache, so 10 looked generous. Run 30439640107 then hit "Failed to restore: Cache service responded with 400", Linux installed torch from scratch, and the leg was killed at 10m17s. The 65s was the cache, not the platform — my per-leg split was reasoning from the wrong baseline. A cache miss is not rare enough to treat as an outage (the cache service 400s, a lockfile change invalidates the key, a new runner image starts empty), and a timeout here is self-perpetuating: the leg dies before the post-step saves the cache, so the next run is cold too. 25 everywhere, still bounded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3553965f41 |
ci(windows): make the ffmpeg retry test the outcome, not choco's exit code (#1290)
* ci(windows): make the ffmpeg retry test the outcome, not choco's exit code The chocolatey feed 503'd; choco printed "Unable to find package 'ffmpeg'" and "installed 0/0 packages" — then exited 0. The retry loop added on 2026-07-20 for this exact class was `choco install ... && break`, so it broke out on attempt 1, no backoff ran, and the job died one line later on `ffmpeg: command not found`. It took #1281 red on an unrelated change. A retry that trusts a lying exit code is not a retry. The loop now exits on `command -v ffmpeg` and still fails the job loudly when ffmpeg never arrives. Tests extract the real step body from ci.yml and run it against a stubbed choco; 2 of the 4 fail against the previous loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(ci): pin PATH to the stub dir so the retry test can't false-green The harness inherited the ambient PATH, so a real ffmpeg satisfied `command -v` and the loop exited on attempt 1 — every assertion passed against a broken workflow. It happened twice: /opt/homebrew/bin locally, then /usr/bin on the Linux runner, which is what took this PR red. PATH is now the stub dir alone, with the few real tools the stubs need symlinked in, and stub shebangs are absolute (`/usr/bin/env bash` cannot resolve bash when PATH is one directory). test_harness_actually_hides_ffmpeg asserts the sandbox is a sandbox, so the next leak fails loudly instead of quietly passing. 2 of 5 fail against the old `&& break` loop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci: give the Windows smoke leg its own timeout, per-leg not shared Smoke (Windows) has been dying at 10m08s inside `uv sync`, and the shared 10-minute budget made it self-perpetuating: the leg is killed before the post-step saves the uv cache, so the next run starts cold and dies the same way. Nothing primes the cache, so it never gets faster. Measured on run 30385710466 — Linux 65s, macOS 65s, Windows still installing torch when the job was killed. Windows now gets 25 minutes, priced for one cold install to finish and populate the cache; warm runs land nowhere near it. Per-leg rather than raising the shared value, so a genuine hang on Linux or macOS still fails fast instead of inheriting Windows' allowance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(windows): skip the backoff after the final attempt; tighten the tests CodeRabbit, both valid: - The loop announced "retrying in 90s" and slept after attempt 3, though no fourth attempt exists — 90s added to an already-doomed job. - The retry tests asserted `attempts >= N`, so a regression that kept going after ffmpeg appeared would still pass. Pinned to exact counts, plus a case asserting the final attempt announces no retry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
d49c56df4a |
fix(shell): root error boundary so an app-render throw can't blank the window
The mount chain (StrictMode → QueryClientProvider → RemoteAuthGate → App) had no error boundary above App. App's per-tab <ErrorBoundary> wraps only cover their own subtrees, so a throw in App's own render — its top-level hooks, store access, restoreProjectExtras, or the header/sidebar chrome that renders before any tab — escaped every boundary and left #root empty. The shell's blank_guard could then only reload three times and paint a dead-end failure page: exactly the "#root children = 0 after 3 reloads" a user hit. Wrap the mount root in <ErrorBoundary name="app-root"> so such a throw shows the in-app, recoverable error card (Reload / Report, data untouched) instead of a blank window. The shell guard stays as the last resort for the rarer case where even the boundary can't render. Recurrence-proofing (the throw only had to happen once to blank the app, and the #1178-class variant only bites the MINIFIED bundle that dev + jsdom never run): - main-app.test.jsx: fail-before/pass-after — a throwing app tree must leave #root non-empty (the recovery card), not blank. - Wire the existing production-bundle smoke (e2e-prod/prod-bundle-smoke.spec.ts) into CI so a pre-render crash in the shipped bytes fails the pipeline, not a user's launch. Fixed its IGNORABLE list to treat the no-backend WebSocket handshake error as the harness artifact it is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
62afc20525 |
ci: retry chocolatey ffmpeg install — community feed 504s intermittently
Co-Authored-By: Claude Fable 5 <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> |
||
|
|
9ecb810946 |
fix(shell): version-gate crash markers, pin the WebView repair contract, run the Rust suite in CI (#1145)
* fix(shell): version-gate crash markers, pin the WebView repair contract, run the Rust suite in CI Two deferred items from the recurrence audit, plus the CI gap that made them possible: - crash.rs: a persisted "backend crashed" marker now only surfaces for the release that wrote it. After an upgrade, markers from the previous version (quite possibly the build whose crash the upgrade fixed) are ignored and pruned on read instead of resurfacing unacknowledged as if the new build had crashed. backend_version gains #[serde(default)] so legacy version-less markers still deserialize — as "", which the gate treats as stale by design. Preview stamps (X.Y.Z-N) count as their release. - commands.rs: the #879 WebView2 cache repair's filesystem half is extracted into clear_webview_cache_at() (paths + retry policy as parameters, zero behavior change) and its contract is pinned by tests: no marker → nothing touched; marker consumed first, unconditionally (one-shot — a failing repair can never loop across launches); missing cache is success; a locked cache is retried then abandoned with a log, never bricking startup. - ci.yml: the Tauri shell check only ran `cargo check`, which neither compiles nor runs #[cfg(test)] code — so the shell's ~90 unit tests (crash.rs, reset.rs, bootstrap.rs, …) never executed anywhere in CI. `cargo test --lib` now runs them natively on all three OSes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(shell): crash-notice read path is strictly read-only — a prune-save there could destroy a fresh marker Greptile's P1 is real, and hotter than stated: get_last_backend_crash is not just a startup check — streamDropError (#1119) polls it every second for 8 s after a stream drops, which is exactly when the death watcher is inside record_crash's load→push→save. The previous commit's read path did load→prune→save when stale-version markers existed (the post-upgrade state), so a poll could load the pre-crash snapshot, lose the race, and save over the freshly recorded marker — silently deleting the only evidence of the crash it was being polled to find. Smallest fix: reads never write. The read path (extracted as read_notice_from(path, version) so the contract is testable) filters stale-version markers in memory only; disk pruning stays on the write paths (record_crash, acknowledge_backend_crash), where load-modify-save already existed pre-PR and is paced by a crash or a user click rather than a 1 Hz poll. Regression test pins the file as byte-identical across reads, stale markers filtered and current ones surfacing as before. 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> |
||
|
|
3f62471bad |
test: pay down export-router test debt + kill two test-order pollution classes (#1081)
Four pieces of test debt, root-caused and hardened:
1. exports.py test coverage (was: zero dedicated tests): new
tests/test_exports_api.py (26 tests) covering /export, /export/record,
/export/history, /export/reveal — happy paths, traversal/containment
guards (incl. symlink escape), destination validation, error mapping,
and the mp4 watermark-overlay branch with its plain-copy fallback.
Two real bugs found and fixed in the router:
- _safe_destination checked isabs() on realpath()'s output, which is
always absolute — dead check; a relative destination silently exported
to a cwd-dependent location instead of the documented 400.
- _safe_source let "." / ".." through the basename guard (caught only
later by realpath containment as a confusing 404); now 400 up front.
2. CI-Linux fp16 default-dtype leak (test_prefers_vocals_over_mix,
test_final_dub_track_and_seg_wav_are_watermarked): not reproducible on
macOS — instrumenting torch.set_default_dtype across both tests records
zero non-fp32 sets locally. Both tests now carry an opt-in
torch_dtype_isolation fixture (save/restore, so the leak can never
spread), and the conftest guard is demoted to pure insurance. A cheap
permanent recorder wraps torch.set_default_dtype /
set_default_tensor_type once torch appears and captures the setter's
stack only on a non-fp32 set; both fixtures print that stack when they
fire, so the next CI occurrence names the exact culprit call chain.
3. Test-order pollution (both reported combos): root cause was
collection-time sys.modules stubbing in backend/tests — seven modules
installed bare ModuleType stubs for core.config (and test_capture_ws.py
for services.model_manager/asr_backend/ffmpeg_utils, now all lazily
imported by the router anyway). pytest imports test modules during
collection, so the stubs leaked process-wide before any test ran:
- combo (a): monkeypatch.setattr("core.config.OUTPUTS_DIR", ...) in
test_longform_e2e died with AttributeError (core never gets a .config
attribute when the import is satisfied straight from sys.modules).
- combo (b): test_router_smoke's `from main import app` died with
ImportError: cannot import name 'find_ffmpeg' (unknown location).
Fix at source: new backend/tests/conftest.py sets a hermetic
OMNIVOICE_DATA_DIR (mirroring tests/conftest.py, #878) and the real
core.config is imported everywhere — zero sys.modules surgery. New
backend/tests/test_no_module_stubs.py guards the whole class (verified
fail-before/pass-after against the old stub). Stale rationale comments
in pyproject.toml and ci.yml updated to match.
4. batched_tts.py TODO(#312): investigated, comment corrected only —
#312 is closed (the live routes are engine-aware); this module has zero
call sites and stays an unintegrated experiment. See PR notes.
Full tests/ suite: 2796 passed. backend/tests standalone: 130 passed.
Both pollution combos re-run green in the reported orderings.
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> |
||
|
|
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> |
||
|
|
5c9b7ca313 |
chore(format): adopt oxfmt for JS/TS/JSX + CI format gate (#774)
Adds oxfmt (Rust formatter, Prettier-conformant) — the repo had no formatter, so this is a one-time normalization of the JS/TS/JSX code (257 files; purely cosmetic — full suite stays 638/638). Scope is deliberately narrowed in .oxfmtrc.json to JS/TS/JSX only: - singleQuote:true + jsxSingleQuote:false — preserve the project's existing style (single-quoted JS, double-quoted JSX attrs), not oxfmt's double-quote default. (Flipping quotes globally also broke a source-string-parsing test; preserving them keeps featureCoverage green.) - Excludes **/*.css (the CSS→Tailwind migration will rewrite those — formatting them now is wasted churn), **/*.json (avoids reformatting 20 i18n locale files + config), **/*.toml, and src-tauri/** (Rust/Tauri config — out of scope for a frontend JS formatter; oxfmt was reformatting Cargo.toml/tauri.conf.json). Tooling: - `bun run format` (write) / `bun run format:check` (verify). - ci.yml: new "Frontend format check (oxfmt)" gate after the oxlint gate. Verified: format:check clean; oxlint 0 errors; vite build; full suite 638/638; bun install --frozen-lockfile in sync. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8765f766e1 |
chore(lint): adopt oxlint as the linter + CI gate; fix the bugs it surfaced (#761)
ESLint was misconfigured (only globals.browser → 47 false no-undef) and run NOWHERE in CI, so 259 errors had accumulated unnoticed. Replace it with oxlint (Rust, ~50-100x faster) as the primary linter AND a real CI gate so lint debt can't silently pile up again. Tooling: - frontend/.oxlintrc.json — correctness=error; no-unused-vars with the existing ^[A-Z_] convention; node/vitest env overrides + AudioWorklet/__APP_VERSION__ globals (kills the false no-undef class); max-lines:500 (warn). - package.json: `lint` → oxlint, `lint:fix`, `lint:hooks` (advisory eslint). - eslint.config.js stripped to ONLY the React-Compiler rule family oxlint can't do yet (set-state-in-effect etc.), run via `lint:hooks`, NOT gated. Drop once oxlint's JS-plugin support leaves alpha. - ci.yml: new "Frontend lint (oxlint)" step in the Tests job — the gate. Real bugs oxlint caught (were buried in ESLint's noise): - GlossaryPanel: <X/> close-icon used but never imported → the edit-row cancel button threw ReferenceError at render. Imported X. - Two use*-named NON-hooks (useEngine action, useArchetypeAsProfile API call) tripped rules-of-hooks; suppressed with documented disables (renaming these misleading names is a worthwhile follow-up). Cleanup to reach a 0-error gate: removed 52 genuinely-dead vars/imports across 18 files (heavy in App.jsx — stale useState left over from prior refactors) and 4 behavior-preserving autofixes (no-useless-fallback-in-spread / no-useless-escape). Verified: oxlint 0 errors; bun install --frozen-lockfile in sync (Docker rule); vite build passes; full suite 638/638. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
bc15fdc990 |
fix(deps): re-sync root bun.lock after #485 (fixes main Docker red) (#492)
* fix(deps): re-sync root bun.lock after #485 frontend floor bumps (main Docker red)
#485 bumped ~25 dependency floors in `frontend/package.json` but didn't
regenerate the repo-root `bun.lock` (this is a bun *workspace* monorepo — the
lockfile lives at root and embeds the frontend member's ranges). The Docker
workflow runs `bun install --frozen-lockfile`, which failed on the drift
("lockfile had changes, but lockfile is frozen") — turning main red on commit
|
||
|
|
ee671300be |
ci: gate omnivoice-tts build to pin changes; drop hanging Intel-Mac leg (#147)
* ci: gate omnivoice-tts build to pin changes; drop hanging Intel-Mac leg The omnivoice-tts C++ runtime is pinned to a commit SHA in quant_map.json, so it only needs rebuilding when that pin (or the build script) changes — not on every PR/push. Running it per-push left the heavily-contended hosted macOS runners (esp. Intel macos-13) sitting in "Waiting for a runner…" for hours as a perpetual queued check (the UNSTABLE state on every PR). - Moved the build out of ci.yml into its own workflow, .github/workflows/build-omnivoice-tts.yml, gated to: paths [quant_map.json, scripts/build-omnivoice-tts.sh, the workflow] + workflow_dispatch. Normal PRs no longer trigger (or hang on) it. - Dropped the Intel darwin-x86_64 (macos-13) matrix leg: that hosted pool is unusably contended and Apple's momentum is on arm64; Intel-Mac users get the in-process OmniVoiceBackend fallback (already the documented behavior). Kept linux-x86_64, windows-x86_64, darwin-arm64. Re-add macos-13 here if first-class Intel binaries are ever needed. Both workflows YAML-validated. Matches ci.yml's stated philosophy of keeping heavy platform builds off the per-PR path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: timeout-minutes + injection-harden the omnivoice-tts build (bot review) - Greptile: add `timeout-minutes: 45` so a hung leg (esp. experimental darwin-arm64 Metal) can't run to GitHub's 6h ceiling — same resource-drain class this PR addresses. - CodeRabbit: stop interpolating the pinned SHA / platform directly into the run block. Validate the SHA is a git hash in the pin step, then pass it + platform via quoted env vars (no shell-injection surface from quant_map.json). Declined: SHA-pinning actions@v4 — matches the repo's floating-tag convention (ci.yml/release.yml); belongs in a repo-wide hardening pass + Dependabot. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b34dcd9e11 |
Phase 4 Plan 04-01: SPIKE-01 GGUF — GO + integration (#100)
* Phase 4 Plan 04-01: SPIKE-01 GGUF — GO + Wave 1 integration Integrates Serveurperso/OmniVoice-GGUF as a hardware-adaptive default voice-cloning engine, with overridable fallback to the in-process OmniVoiceBackend. Spike confirmed GO: the model is a clean quantization of k2-fsa/OmniVoice (Apache-2.0 + MIT runtime, `omnivoice-lm` custom architecture so it does NOT load in vanilla llama.cpp). Pinned SHAs: * Serveurperso/OmniVoice-GGUF revision: 361609388ae572a820d085185bbbe2a2aac4b30e * ServeurpersoCom/omnivoice.cpp master: 886fc079838ca7400cb2b42b36e2a65aa1daabe8 Implements GGUF-01 (hardware probe) through GGUF-05 (default-engine resolver with graceful fallback). The four `bin/omnivoice-tts-*` artifacts are committed as zero-byte placeholders; the new CI matrix job builds the real binaries per platform from the pinned commit SHA and appends a SHA-256 manifest used by `is_available()` for tampering detection (T-04-01). The macos-14 (Apple Silicon) slot is marked `continue-on-error: true` because omnivoice.cpp publishes no `buildmetal.sh` (Pitfall 1 / Assumption A1) — failure feeds into Task 3's GO/NO-GO call. Quant override is allow-listed against quant_map.json entries only (T-04-05). Argv is composed from typed Path objects rooted in HF_HUB_CACHE; never uses `shell=True`. HF token redaction applies to captured stderr before logging (AUTH-05 / T-04-04). Tests: 36 new (8 hardware-probe + 13 GGUF engine + 6 settings_store quant override + grep gate); 428 passed in full suite vs 402+ baseline. ADR Status stays "Proposed (research-supported)" — Task 3 (human checkpoint) flips to Accepted after CI produces real binaries and a reviewer signs off on the GGUF-06 cross-hardware smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: install libopenblas-dev on linux-x86_64 omnivoice-tts build The pinned omnivoice.cpp commit (886fc079...) ships a `buildcpu.sh` that passes `-DGGML_BLAS=ON`. ubuntu-latest has no BLAS implementation preinstalled, so the cmake configure step fails with `Could NOT find BLAS (missing: BLAS_LIBRARIES)` and the job exits in 13 s before producing the linux-x86_64 binary. macOS (Accelerate, built in) and Windows (BLAS off by default in the ggml CMakeLists for non-APPLE platforms — the build script doesn't invoke buildcpu.sh on those slots) are unaffected and stay green. Adds a Linux-gated apt step to install libopenblas-dev + pkg-config before the build, restoring cross-platform parity per the CLAUDE.md "default features must work on every platform" rule. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(gguf): constrain ref_audio to project roots — block /etc/shadow on Linux The GGUF engine's `_build_argv` previously validated ref_audio only via `ref_path.is_file()` — i.e. "does this path exist?" That check is platform-dependent: `/etc/shadow` doesn't exist on macOS (rejected naturally), but it IS a real system file on Linux, so the validation silently accepted it. CI's ubuntu-22.04 runner exposed the gap via `test_generate_blocks_freeform_ref_audio`, which exists precisely to guard the "freeform ref_audio path" attack surface. Fix: confine ref_audio to one of three allowed roots before existence checks: - VOICES_DIR (user-saved voice profiles) - DUB_DIR (per-job auto-clones extracted from source video) - tempfile.gettempdir() (browser-upload temp files; existing `cleanup_ref` flow in generation.py) Anything outside those roots → FileNotFoundError, matching the existing failure-mode contract callers handle. Existence check still runs after, so the test's mocked subprocess.run is never reached and the test passes deterministically on all three platforms. Cross-platform parity (per CLAUDE.md 2026-05-20 rule): identical behaviour on macOS / Windows / Linux — the allow-list is computed from core.config which uses platform-specific path resolution but yields the same logical "project tree" on every OS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(gguf): mark darwin-x86_64 binary build as experimental GitHub's macos-13 (Intel) runner pool is heavily contended — PR #100 queued for 30+ minutes waiting on darwin-x86_64 while every other platform finished in ~1m. Intel Macs are also fading hardware (Apple's platform momentum is entirely on Apple Silicon), and the GGUF engine's runtime already handles a missing binary gracefully (`is_available()` returns False on Intel Mac with a "binary not bundled for this platform" message, same path used for first-launch before any binaries build). `experimental: true` mirrors what darwin-arm64 (Metal) already has — slot still runs and uploads its binary when successful, but a failure or runner backlog no longer blocks merges. Keeps the GGUF engine shippable across the dominant arm64 / Linux / Windows surface without holding the inbox on a slow-runner queue. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
93aa66ab0a |
Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend (#101)
* Phase 3 Plan 03-01: Supertonic-3 engine on SubprocessBackend
Adds Supertonic-3 as a 7th opt-in TTS engine on the Phase 2
SubprocessBackend primitive. Closes TTS-01..06 (REQUIREMENTS.md):
* TTS-01 — _REGISTRY["supertonic3"] resolves to Supertonic3Backend,
a SubprocessBackend subclass.
* TTS-02 — `supertonic==1.3.1` lives under [project.optional-dependencies];
default `uv sync --no-dev` does NOT install it. Exactly one
`onnxruntime` row in `uv pip list` after `--extra supertonic`.
* TTS-03 — Model revision pinned by 40-char commit SHA
(724fb5abbf5502583fb520898d45929e62f02c0b — the "Initial
Supertonic 3 release" SHA, same as the SDK's own pin).
Resolver script for intentional bumps:
scripts/resolve_supertonic3_sha.py.
* TTS-04 — Honest CPU-only reporting. `is_available()` message says
"ready (CPU-only via onnxruntime)" and never mentions
"cuda" or "mps". `gpu_compat = ("cpu",)`.
* TTS-05 — License gate via settings_store helpers
(get/set_license_accepted) + Loopback-only
/api/settings/license endpoint + SupertonicLicenseDialog
frontend modal showing MIT (code) and OpenRAIL-M (model).
Wired into EngineCompatibilityMatrix as an "Accept license"
button on rows whose `reason` mentions "license not
accepted".
* TTS-06 — 3 langs (en/ja/ru) × 3 sec smoke test in
tests/test_supertonic3.py::test_smoke_3langs_3sec
(OMNIVOICE_SMOKE-gated; asserts no onnxruntime-gpu row
post-synthesize).
Package legitimacy gate (Task 1 in plan): supertonic on PyPI verified
to be published by Supertone Inc. (ato@supertone.ai), repo
github.com/supertone-inc/supertonic, wheel is pure-Python with no
postinstall scripts. Same publisher ships supertonic-js on npm under
the same maintainer email.
Test results:
* tests/test_supertonic3.py — 10 passed, 3 skipped (network-gated).
* tests/smoke/ — 4 passed.
* tests/ (full, --ignore=tests/manual) — 412 passed, 0 failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(tests): uv sync --all-extras so optional-engine tests can import their package
Phase 3 added `supertonic` as an optional dependency. The CI Tests job
runs `uv sync` (no extras), so `test_cpu_only_honest` and `test_license_gate`
in tests/test_supertonic3.py hit the "supertonic package not installed"
fallback instead of the real import path, and fail.
Bare `uv sync` is the right default for users (engines are opt-in), but
the test environment should exercise the full surface. `--all-extras`
keeps the smoke job lean (still bare `uv sync`) while letting Tests
verify the integrated behavior of every optional engine.
Future-proofs against the same failure mode in Phase 4 (GGUF) and any
later optional engines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
715766cb04 |
Phase 1 Wave 2: per-OS install docs + Settings UI + error→docs deeplinks (#94)
* docs(install): per-OS install pages + drift validator + CI gate
Splits the 600-line README install section into self-contained per-OS docs
under docs/install/{macos,windows,linux,docker}.md plus a Top-10
troubleshooting index. Each OS doc is end-to-end: a user opens it and
reaches a working app following only commands inside that file.
Adds:
- docs/install/{macos,windows,linux,docker}.md (OS-specific install paths)
- docs/install/troubleshooting.md (top 10 install errors)
- docs/engines/cosyvoice.md (closes #55 docs half)
- docs/features/diarization.md (pyannote license flow)
- docs/setup/huggingface-token.md (3-source cascade guide)
- scripts/validate-install-docs.py (INST-06 docs-drift gate)
- tests/scripts/test_validate_install_docs.py (B-5: validator self-tests)
- .github/workflows/ci.yml step running the validator on every PR
Implements INST-02 (README routing), INST-03 (macOS Gatekeeper anchor),
INST-12 docs half (Windows torch-compile-oom anchor), DOCS-01..05.
The validator is a one-way diff: every `<!-- validate -->`-tagged line
in docs must appear in scripts/desktop-prod.sh after normalisation
(prompt-prefix strip, CRLF, trailing whitespace, blank-and-comment skip).
A `<!-- validate: skip -->` marker opts out for human-readability blocks.
Its own 10 unit tests catch regressions in the gate itself.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(deeplinks): links.py + error_docs_map (Python + TS mirror)
Adds the single source of truth for the project repo URL and the 4-class
error → docs taxonomy that both the in-app ErrorBoundary deeplink button
(Wave 2 Task 3) and the Phase 5 bug reporter will consume.
New:
- backend/core/links.py — PROJECT_REPO_URL + BLOB_MAIN resolver
(Tauri config first, pyproject fallback)
- backend/core/error_docs_map.py — lookup(error_class) → docs URL
- frontend/src/utils/errorDocsMap.ts (TS mirror with classifyError helper)
- tests/backend/core/test_links.py + test_error_docs_map.py
- frontend/src/utils/errorDocsMap.test.ts
Resolves checker B-6 (links.py ownership) and Open Question #3 (which fork
the deeplinks resolve to — the Tauri updater endpoint wins, which points
at the desktop app fork debpalash/OmniVoice-Studio).
The TS BASE constant is documented as the second hardcoded URL drift site;
the keys-sync test (`test_keys_match_python_map` equivalent) guards the
4-class taxonomy contract between Python + TS halves.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ui): Settings → API Keys panel + ErrorBoundary docs deeplink
Wave 2 AUTH-03 UI half + ErrorBoundary deeplink wiring.
ErrorBoundary fallback now renders an "Open docs for this error" button
that classifies the thrown Error message (heuristic: pkg_resources → 401 /
HfHubHTTP → WebKit / white screen → quarantine / Gatekeeper) and opens the
matching docs anchor via Tauri shell.open (with a window.open fallback
in browser dev mode).
ApiKeysPanel consumes the Wave 1 resolver state endpoint:
- 3 source rows (App / Env var / HF CLI) with set/unset indicator,
masked token preview, whoami username + green check
- "Active" badge on whichever source is currently serving the cascade
- App-row only: Save (POST /api/settings/hf-token) +
Clear (DELETE with optional "also clear HF CLI" confirm dialog)
- "Test now" button refetches state (invalidates the resolver's
validation cache via the same endpoint hit)
Panel mounted in the existing Settings → Credentials tab; the legacy
HF_TOKEN row from CREDENTIAL_FIELDS is filtered out so the two paths
don't fight over the same key.
Threat T-02-02: the panel never displays the full token. The masked
value comes from the resolver state endpoint; the full token only
crosses the IPC boundary on Save (POST) and is cleared from local
state on success.
Closes AUTH-03 fully (Wave 1 backend + this Wave 2 UI).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(perf): INST-12 Disable torch.compile (Windows) toggle (backend + UI)
Wave 2 Task 4 — full INST-12 delivery per checker B-2/B-7 v0.3.0 fat-release
decision. Both the docs half (windows.md anchor, shipped in earlier commit)
and the runtime toggle are now in Phase 1.
Backend:
- backend/services/settings_store.py: adds get_text/set_text helpers for
non-secret config (refuses to write to the encrypted hf_token key).
- backend/api/routers/settings.py: GET + PUT
/api/settings/perf/torch-compile-disabled, both under the existing
loopback guard (threat T-02-04).
- backend/services/engine_env.py: new `build_engine_env()` helper that
centralises HF_TOKEN/YOUR_HF_TOKEN injection from the 3-source resolver
AND injects TORCH_COMPILE_DISABLE=1 when the flag is set on win32.
Phase 2 SubprocessBackend launchers should adopt the same helper.
- backend/services/sonitranslate.py: migrated to engine_env.build_engine_env()
while preserving the source-level `env["HF_TOKEN"]` sentinel that
test_sonitranslate_module_uses_resolver checks.
Frontend:
- frontend/src/components/settings/PerformancePanel.{jsx,css,test.jsx}:
toggle UI with the explainer for #65; renders disabled with a "not
applicable" badge on macOS/Linux.
- frontend/src/pages/Settings.jsx: mounts the panel into the Credentials
tab alongside the API Keys panel.
Tests:
- tests/backend/test_perf_settings.py: 7 backend tests (default state,
PUT persistence, T-02-04 non-loopback rejection, settings_store round-
trip, env injection on win32, NO injection on macOS/Linux, NO injection
when disabled).
- frontend PerformancePanel.test.jsx: 5 tests (renders from GET state,
PUT on toggle, disabled on non-Windows platforms, pre-enabled state).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(planning): Wave 2 SUMMARY + REQUIREMENTS status updates
- .planning/phases/01.../01-02-SUMMARY.md: full implementation report
per template (truths, commits, tests, deviations, drift-site
acknowledgments per W-3, launcher seam name for Phase 2,
taxonomy keys for Phase 5).
- .planning/REQUIREMENTS.md: flips Wave 2 closures to Done:
AUTH-03, INST-02, INST-03 (docs half), INST-06, INST-12,
DOCS-01..05.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e4dbf4c8c0 |
P0: release.yml typecheck + bind audit + loopback middleware (#84)
Three P0 fixes bundled — foundation cleanup before v0.3.0 phase work. Closes release.yml drift (PR #51's tabs broke v0.3.0 tag releases), production bind exposure (Critic F1), and 9-endpoint LAN gap on /system/* (Critic F2+F3). 5 new tests; 243 full pass. |
||
|
|
766e2f7284 |
Phase 0 — Gates: cross-platform CI matrix + regression fixture + release smoke (#71)
* docs: initialize OmniVoice stabilization milestone project * chore: add project config (yolo + balanced) * docs: domain research for stabilization milestone * docs: define v1 requirements for stabilization milestone * docs: add GGUF + singing engine spike requirements (Phase 4 new) * docs: roadmap revision + CLAUDE.md (7 phases, 62 reqs, +GGUF/SING spikes) * docs(phase-0): add Gates phase RESEARCH.md Phase 0 research synthesizes the cross-platform CI matrix, frozen omnivoice_data fixture, installer post-build smoke, SHA-256 checksum publishing, and PR-template extension into copy-paste-ready YAML and Python snippets composed entirely from existing in-repo patterns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(phase-0): add Gates phase CONTEXT, PATTERNS, and PLAN Phase 0 — Gates is the hard pre-condition for v0.3.x stabilization. Lays cross-platform CI matrix (macos-14/windows-2022/ubuntu-22.04), regression fixture (≤200 KB), installer smoke on tag push, SHA-256 checksums in release body + per-OS SHA256SUMS-*.txt assets, PR template with RC cadence + fixture line, and the open-PR landing for #51. Plan covers GATE-01..06; structured into 7 slices (A–G) with explicit Slice C → Slice G dependency reordering so the new smoke-matrix lands on main before PR #51 (CONTEXT.md L86 interleave decision). Plan-checker iteration 2: APPROVED — all 3 BLOCKERs + 3 MAJORs from iteration 1 resolved (file truncation/Slice-G missing, GATE-06 sibling PR verification, Slice C ordering, Truth #5 wording, macOS Tauri WebView avoidance per Pitfall #5, Windows taskkill per Pitfall #2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(00-gates): seed regression fixture (GATE-01) - scripts/seed-test-fixture.py — deterministic builder for tests/fixtures/omnivoice_data/ - wipes + rebuilds; fixed created_at=1700000000.0; all-zero PCM for byte-deterministic diffs - calls backend.core.db.init_db() directly (alembic versions/ is empty — see CONTEXT.md) - checkpoints WAL → DELETE on close so no -shm/-wal sidecars pollute git status - exits non-zero if fixture > 200 KB - tests/fixtures/omnivoice_data/{omnivoice.db, README.md} — 8-table empty DB + 1 voice_profiles row - tests/fixtures/omnivoice_data/voices/test-voice/{profile.json, sample.wav} — 1-sec 24 kHz mono silence - .gitignore — explicit allow-list (!tests/fixtures/omnivoice_data/**) so the existing omnivoice_data/, *.db, *.wav patterns don't hide the fixture from git Verifies: du = 144 KB on disk; sqlite_master lists 8 init_db tables + sqlite_sequence; voice_profiles has exactly 1 row id='test-voice'; 0 rows in generation_history. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(00-gates): add tests/smoke/test_boot_smoke.py (GATE-01) - tests/smoke/__init__.py — package marker so pytest treats tests/smoke/ as a module - tests/smoke/test_boot_smoke.py — 4 in-process FastAPI TestClient smoke tests: * test_health_returns_ok — /health returns 200 + {status:ok, device:...} * test_profiles_endpoint_lists_fixture_voice — /profiles surfaces the seeded test-voice row (validates OMNIVOICE_DATA_DIR wiring → DB_PATH → init_db schema) * test_system_info_includes_data_dir — /system/info resolves data_dir * test_history_endpoint_empty — /history reaches DB and returns [] Test isolation env vars (OMNIVOICE_MODEL=test, OMNIVOICE_DISABLE_FILE_LOG=1) set at module top BEFORE any backend import — pattern from tests/test_router_smoke.py. Fixture is copied to a per-session temp dir so the test never mutates the checked-in artifact (SQLite file-change counter + runtime subdirs like dub_jobs/ would otherwise dirty `git status` after every run). Failure mode: if tests/fixtures/omnivoice_data/ is missing, pytest.fail at import time with the regenerate command. - .gitignore — tighten the GATE-01 allow-list to ONLY the seed-produced files (README.md, omnivoice.db, voices/test-voice/profile.json, sample.wav). Prevents future runtime subdirs the backend may create under the fixture from being accidentally committed. Verifies: `uv run pytest tests/smoke/ -q --tb=short` → 4 passed in 1.31 s (target was < 30 s). `git status` clean after a test run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(triage): record post-planning GitHub state — PR #62, new issues, OOS deferrals - GATE-06: mark #53 + #61 merged (2026-05-16); add #62 (Wave 1 quick wins) to gate set - INST-01: note PR #62 implements setuptools pin (closes #58) - INST-04: note PR #62 lands README docs for #56 workaround - INST-12: new requirement for #65 Windows Triton/torch.compile OOM (filed post-planning) - Out of Scope: defer #67/PR #68 (audio effects), #64 (custom model dir), PR #66 zh-CN (i18n milestone), #63 (empty-template bug) PR #62 is the user's own Wave 1 work landed as a separate PR while GSD planning ran in parallel. Merging it eliminates duplicate work in Phase 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(00-gates): add cross-platform smoke matrix (GATE-02) - New smoke-matrix job on macos-14, windows-2022, ubuntu-22.04 - needs: test, fail-fast: false, timeout-minutes: 10 - Pinned actions: checkout@v4, setup-python@v5, setup-uv@v3 (cache enabled) - Per-OS ffmpeg + libsndfile install (brew/choco/apt via awalsh128 cache) - UV_HTTP_TIMEOUT=120, UV_HTTP_RETRIES=5 for restricted-network resilience - Narrow scope: uv run pytest tests/smoke/ -q --tb=short - Existing `test` and `tauri-cross-platform` jobs untouched Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: add workflow_dispatch to ci.yml so smoke-matrix can run on feature branches * feat(00-gates): add --health-check CLI flag to backend entrypoint (GATE-03) - argparse on __main__ block; --health-check boots uvicorn in a daemon thread and polls http://127.0.0.1:3900/health every 5s for up to 60s. - Prints 'OK — /health responded 200 after Ns' and exits 0 on first 200. - Prints 'FAIL — /health did not respond 200 within 60s' to stderr and exits 1 on timeout. Default invocation behavior unchanged. - No new deps (stdlib argparse/threading/time/urllib.request/sys + uvicorn). - Consumed by per-OS installer-smoke step in .github/workflows/release.yml. Verified locally: exits 0 in 5s against tests/fixtures/omnivoice_data/. * ci(00-gates): add per-OS installer smoke to release.yml (GATE-03) Adds three matrix-leg-specific steps after 'Build + release (Tauri)', each gated by runner.os with timeout-minutes: 5: - macOS (macos-14): hdiutil attach DMG → locate bundled Python backend inside *.app/Contents (NOT the Tauri WebView shell — RESEARCH Pitfall #5: WebView hangs on headless runners) → invoke --health-check → hdiutil detach. Falls back to *.app/Contents/Resources and hard-fails with a directory listing if no backend binary found. - Windows (windows-2022): msiexec /quiet install → find backend.exe under 'C:/Program Files/OmniVoice Studio' → invoke --health-check in background, wait, then taskkill //F //T //PID to cleanup orphaned PyInstaller child processes on port 3900 (RESEARCH Pitfall #2). - Linux (ubuntu-22.04): --appimage-extract (no FUSE on GH runners), locate binary or AppRun, run under xvfb-run -a. Bundle-only regressions (PyInstaller missing-module, Tauri sidecar path mismatch) are invisible to ci.yml's in-process smoke matrix — this step closes that gap before any release is published. Verified: YAML parses; all three steps present; gating + timeout correct; Pitfall #2/#5 mitigations preserved. * ci(00-gates): publish SHA-256 checksums in release body + as asset (GATE-05) - Add 'Compute SHA-256 checksums' step writing SHA256SUMS-<label>.txt per matrix leg using native shasum/sha256sum (Git Bash on Windows). - Add 'Append checksums to release + attach SHA256SUMS file' step using softprops/action-gh-release@v2 with append_body: true so the hashes land in the release body alongside tauri-action's content (not replacing it) and the file is uploaded as a release asset for 'shasum -c SHA256SUMS-<label>.txt' verification. - Both steps gated by 'github.event_name == push && refs/tags/v*' so workflow_dispatch dry-runs do not attempt to attach to a non-existent release (per CONTEXT.md L70 + RESEARCH Pitfall #7 deferral of any aggregate cross-leg SHA256SUMS job). - fail_on_unmatched_files: true to surface path-resolution errors loudly. * docs(00-gates): document RC cadence + regression-fixture check in PR template (GATE-04) * docs(setup): add HF token persistence guide for macOS/Windows/Linux (DOCS-05) Covers two persistent paths: - Method A — canonical ~/.cache/huggingface/token via huggingface-cli login - Method B — shell env var (~/.zshrc / ~/.bashrc / Windows User scope) Documents the v0.2.7 "session only" in-app behavior + notes that Phase 1 AUTH-03 will make in-app pastes write to the canonical file. Bundled with Phase 0 PR per user request. Strictly DOCS-05 scope — zero code changes, no engine touches. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * spec(auth): redesign HF token resolution as 3-source cascade with fallback (AUTH-01..06) Replaces the env_store.py file-based design with a SQLite-backed app store + cascade resolver that checks app → env var → ~/.cache/huggingface/token in priority order, with automatic fallback to next source on HTTP 401. User-explicit design decision: - App-stored token (SQLite settings table, AES-GCM encrypted) wins - Env var ($HF_TOKEN) second - Global huggingface-cli login file third - All three sources visible in Settings → API Keys with "Active" badge - Save action populates BOTH app store AND canonical HF file (defense in depth) New requirement: - AUTH-06 — on 401, auto-retry next source in cascade before erroring Also: traceability count corrected (62 → 74 — undercount at planning + INST-12 + AUTH-06 added post-planning). All 74 v1 reqs mapped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): backend recognizes HF token from canonical file, not just env var Two call sites were only checking $HF_TOKEN env var, missing the canonical ~/.cache/huggingface/token file written by `huggingface-cli login` (or the app's future Save action): - system.py `/system/info` `has_hf_token` flag — UI showed "No HF token" even when `huggingface-cli login` had populated the file. - model_manager.get_diarization_pipeline — pyannote diarization silently returned None when only the canonical file was set. This is the bug behind issue #35 (speaker diarization setup failure). Both fixes use the same pattern: env var > huggingface_hub.get_token() (which reads the canonical file). Adds a local _has_hf_token() helper to system.py with a comment marking it as prelude to the AUTH-01..06 cascade (Phase 1 token_resolver.py will layer SQLite app-store on top). Closes #35 sub-issue (canonical token invisible to diarization). Cross-cuts AUTH-02 + AUTH-06 design for Phase 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dictation): make pill-widget mode reachable from GUI + scripts (INST-13) The dictation widget infrastructure shipped in PR #40 but was only reachable via the undocumented --pill CLI flag. Adds three discovery paths: 1. Tray menu: "Switch to Dictation Widget" (studio mode) — saves launch_as_widget=true to config, relaunches with --pill, exits current. Mirrors the existing "Open Studio" path in pill-mode tray. 2. Persistent config: AppConfig.launch_as_widget (bool, default false). Read at startup via load_config_pre_app() (uses dirs-next, no AppHandle required). CLI --pill still takes precedence when explicitly passed. 3. Tauri commands: get_launch_as_widget / set_launch_as_widget for the Phase 2 Settings UI to bind a checkbox to. 4. Scripts: bun desktop-prod:pill / desktop-prod:run:pill — forward --pill to the bundled app launch. macOS uses `open -n --args` to spawn fresh instance with the flag. Closes the GUI half of INST-13. Phase 2 closes the Settings UI half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dictation): show widget unconditionally on pill-mode launch + visible Suspense fallback Before: pill mode set up correctly but the widget window stayed hidden until ⌘⇧Space was pressed. New users saw absolutely nothing on launch (no main window, no dock icon, hidden widget) and assumed the app failed. If global-shortcut Accessibility permission wasn't granted, they had no path to discover the widget at all. Two changes: 1. lib.rs: in pill_mode_setup, explicitly show + position + focus the widget window after hiding main. With per-call error logging so we can diagnose failures (and a clear error log if widget window wasn't created at all — points at tauri.conf.json regression). 2. main-app.jsx: Suspense fallback was `null`, which combined with widget's transparent+decorations:false config made any lazy-import delay or failure invisible. Now renders a dark pill saying "Loading dictation…" so even if CaptureWidget lazy-import stalls, the user sees the window exists. Studio mode behavior unchanged — widget stays hidden until hotkey or tray click triggers it (existing show() call in the shortcut/ menu handlers is preserved). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(dictation): create widget window programmatically; Tauri 2 silently dropped config-array creation Root cause: declaring the widget window in tauri.conf.json's app.windows[] silently failed in Tauri 2 — get_webview_window("widget") returned None even though the config was syntactically valid. Probable culprit was the transparent + decorations:false + visible:false combo, but Tauri offered no error message either at startup or via webview_windows() enumeration. Diagnosed by adding webview_windows() enumeration logging at setup start (only ["main"] ever appeared) and a programmatic WebviewWindowBuilder fallback that surfaces real Result errors. Fix: - tauri.conf.json: widget entry now has `create: false` to make the config-vs-programmatic handoff explicit. - lib.rs setup(): call WebviewWindowBuilder::new(app, "widget", ...).build() with the exact same surface attributes the config used to declare. - capabilities/default.json: include "widget" in windows array so the new window inherits the same Tauri permissions as main. - tauri.conf.json: remove the invalid `"url": "/?window=widget"` field — WebviewUrl::App takes a path only, query strings aren't supported. Both windows now load index.html. - main-app.jsx: replace URL-query-based widget detection with getCurrentWindow().label === 'widget' via @tauri-apps/api/window. This is the Tauri 2-recommended pattern for multi-window apps and works regardless of URL routing. Closes the immediate UX bug behind the dictation widget being invisible. Builds cleanly + manually verified: pill widget visible on screen at top-center after `bun desktop-prod:pill`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9082705676 |
Post-refactor cleanup: wire fingerprints, drop dead code, scope pytest (#50)
* chore: post-refactor cleanup — wire fingerprints, drop dead code, scope pytest Follow-up to PR #49. Fixes residual issues from the App.jsx hooks split and tightens repo hygiene so a bare `pytest` doesn't foot-gun. Real bug - frontend/src/hooks/useDubWorkflow.js: setLastGenFingerprints lives in useSegmentEditing, not on the store. The previous code called useAppStore.getState().setLastGenFingerprints?.(...) — the optional chain swallowed the missing method, so the "N segments changed" badge never updated after a fresh generate until a project save+reopen. Thread setLastGenFingerprints in from App.jsx; useSegmentEditing() now runs before useDubWorkflow() to make the setter available. Dead code from the refactor - frontend/src/App.jsx: drop unused `showAllProjects` useState and `pushUndo` from the useSegmentEditing destructure. - frontend/src/hooks/useDubWorkflow.js: drop 5 unused selectors (preserveBg, defaultTrack, exportTracks, dualSubs, burnSubs) — the dub-download logic that needs these lives in App.jsx, not the hook. Repo hygiene - backend/api/routers/setup.py.bak: delete 38 KB tracked-in-git backup. The setup/ subpackage replacement has been in place for a while. - pyproject.toml: add [tool.pytest.ini_options] with testpaths + norecursedirs. Previously a bare `pytest` would INTERNALERROR walking into research/ (1.2 GB of vendored upstream projects with their own test_*.py files that call sys.exit at module level). - .github/workflows/ci.yml: run backend/tests/ as a second pytest invocation. The 23 tests there stub core.config in sys.modules to avoid the heavy main app import chain — that pollutes import state for other tests, so they need their own session. Previously these tests existed in the repo but never ran on CI. Net effect on lint: 60 → 52 problems (-8) from dead-code removal. Test counts unchanged: pytest 214 + 23, vitest 11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: log silent catch failures that mask real bugs CodeRabbit nitpick on #50: empty catch on the incremental-plan fallback swallows errors. Extending the fix to the catches in this area that have the same problem (a real failure would be invisible) while leaving the genuinely non-actionable cleanup catches alone (EventSource.close(), localStorage.setItem, fire-and-forget UI promises). Logged: - useDubWorkflow.js:97 — transcribe SSE message handler - useDubWorkflow.js:347 — incremental-plan fallback (the CR finding) - useDubWorkflow.js:352 — dub generate SSE event dispatch - App.jsx:552 — exportRecord on Tauri save path - App.jsx:580 — exportRecord on browser download path Left silent (cleanup / non-actionable): - useDubWorkflow.js:68, 112 — evt.close() in SSE teardown - useDubWorkflow.js:102 — SSE error-event payload parse fallback - App.jsx:124 — localStorage.setItem (quota / privacy mode) - App.jsx:789, 901 — fire-and-forget UI promise tails Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
79d4f3b53d |
feat(0.2.6): tray-aware shell, hotkey customization, WS dictation dedupe
Tray + lifecycle: - tauri-plugin-single-instance — second launch focuses existing window instead of racing for port 3900. - Window close hides instead of destroying; backend shutdown moved to RunEvent::ExitRequested so only the tray "Quit" item (or Cmd+Q on macOS) actually exits. - Tray icon flips to red-dot variant during dictation recording. Hotkey customization: - Settings → Capture tab. Records any modifier+key combo, persists to app config, re-registers on launch. - set_dictation_shortcut rolls back to the previous binding on register failure so a bad combo never leaves the user with no shortcut. Dictation latency / correctness: - WS-final treated as source of truth; HTTP POST /transcribe runs only as fallback (WS error / timeout / no-WS path). Audio transcribed once instead of twice. Server accepts an "EOF" text frame (or empty binary frame) so the socket stays open for `final` to be delivered before the client closes. - MediaRecorder chunks queued during the WS handshake are drained in ws.onopen — the server's final transcript no longer drops the first ~250 ms of audio. - Fallback timeout scales with recording length (max(15s, recordedMs+10s)) so long-form dictations don't trip duplicate transcription. Donate page: - Drop Patreon, Bitcoin / Ethereum / Solana cards. Drop qrcode.react. - Move "Commercial License" CTA from page bottom to top-right header bar. Docker hygiene: - docker-compose binds 127.0.0.1 by default. README documents the LAN exposure trade-off + recommends a reverse proxy with auth. CI: - New cross-platform `tauri-cross-platform` job runs `cargo check` against the Tauri shell on macOS / Windows / Linux per PR. Catches platform cfg-gate regressions without paying the full ~15min/platform bundle cost (full bundling stays in release.yml on tag push). Tests: - tests/test_capture_ws.py (3 cases) covers EOF text-frame, empty-binary EOF, and legacy disconnect-finalize paths. Includes the user's previously-staged 0.2.5 polish: cross-platform desktop-prod.sh, Dockerfile base-image fix, bun.lock churn. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
90ef02b2e4 |
chore: unique ports (3900/3901) + broader CI caches (#20)
Two unrelated tweaks grouped into one PR to keep churn low. ## Ports Backend 8000 → 3900, Vite dev 5173 → 3901, 3902 reserved for future IPC. Port 8000 conflicts with Django/Rails/Jupyter/Airflow on most dev machines; the uncommon 3900 range dodges that. Touched: - frontend/src-tauri/src/lib.rs (BACKEND_PORT) - frontend/src-tauri/tauri.conf.json (devUrl) - frontend/vite.config.js (server.port) - frontend/src/api/client.ts (hardcoded API base) - frontend/src/App.jsx (PREVIEW_API fallback) - backend/main.py (CORS allowlist + uvicorn.run default) Rust sidecar launcher and FastAPI uvicorn port stay in sync via the `BACKEND_PORT` constant + explicit port=3900. ## CI caches Build time shaves across ci.yml and release.yml: - `astral-sh/setup-uv@v3` → `enable-cache: true` keyed on uv.lock (~45 s saved per run after uv.lock stabilises) - `awalsh128/cache-apt-pkgs-action` for ffmpeg (~25 s saved) - `actions/cache@v4` on `~/.bun/install/cache` keyed on bun.lock (~15 s saved; applied to both test gate and build matrix) Expected warm test job: ~45-60 s (was ~2-3 min). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c3aa2e4533 |
ci: opt JavaScript actions into Node 24 runtime (#12)
GH deprecates Node 20 for JavaScript actions on 2026-09-16. The deprecation warning surfaces on every run right now. Setting FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at workflow level makes actions/checkout, actions/setup-*, astral-sh/setup-uv, and oven-sh/setup-bun all run on Node 24 without bumping action versions. This is a runtime override only — our own test script still pins Node 22 via actions/setup-node@v4 (required for --experimental-strip-types). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd16354a98 |
ci: invoke node directly for frontend tests; add setup-node to release workflow (#7)
`bun run <script>` auto-aliases `node` to `bun` in script bodies, so `bun run test` fails with "node: bad option: --experimental-strip-types" because bun doesn't support that flag. Call node directly from the CI step instead of going through the package.json script. Also add setup-node to release.yml's test gate — it was missing entirely, relying on bun's node shim. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3c89c8c2d1 |
ci: use node 22 + --experimental-strip-types so node:test can import .ts
CI ubuntu runner shipped node with no TypeScript loader, so
`await import('.../client.ts')` in tests/frontend/apiClient.test.mjs
failed with ERR_UNKNOWN_FILE_EXTENSION. Locally on macOS bun was
handling the extension transparently.
Fix: pin Node 22 via actions/setup-node@v4 (which natively supports
--experimental-strip-types) and pass the flag in the frontend `test`
script. Type annotations in client.ts are stripped at import time,
test bodies stay untouched.
Verified locally with node v24 — 36/36 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
66fb24602b |
ci: add PR-gated test workflow
release.yml only fires on `push: tags: ['v*']` + workflow_dispatch, so the test job it contained never ran on pull requests — PRs landed with no automated test feedback. Split into a dedicated ci.yml that runs backend pytest + frontend node:test + tsc on every pull_request + push to main. release.yml stays tag-only for the heavy 4-platform Tauri matrix build. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |