fe29fe0966dbe7493ea2cd4e781f2d333fbc444f
61
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
93025d9a81 |
fix(dictation): stop the widget stranding an empty square, repair the swept data dirs (#1398)
The dictation hotkey could leave a blank dark square stuck on the desktop with no way to dismiss it. Three defects compounded: the tray listener's effect depended on [state], so it detached across an await on every state change and a press landing in that gap was lost; an idle pill renders null, so the window Rust had already shown was empty; and the opaque chrome background made that empty window a hard-edged square. Nothing could hide it — dismiss() is only reachable from the X button, Esc, or a post-session timer, none of which exist for a session that never started. Fixed at the invariant rather than the call sites: the listener subscribes once for the component's lifetime, the widget window's chrome background is transparent, and an idle-but-visible window reconciles itself to hidden. The reconcile is polled (a dropped press changes no React state, so there is nothing to key an effect off) and aborts if its effect is torn down mid-check, so it can never hide a dictation that has just started. Also in scope: - The rename sweep had repointed three data-dir literals at a brand-named directory that does not exist, so smoke-test.sh verified a directory the backend never writes and desktop-prod.sh silently stopped clearing backend state on Windows. Both invisible on macOS, where they are usually run. A guard test now pins the assignments specifically. - The dictation model picker's download sizes were wrong for all seven models, in both directions — Parakeet TDT v3 (the recommended default) understated 180 MB against an actual 670 MB, while the low-RAM fallbacks were overstated threefold, discouraging exactly the choice that would have helped. Measured from the published repos and pinned by a test. - The 0.6B Parakeet models now decode on more threads, capped by host cores and still overridable. - uninstall.ps1 gained a UTF-8 BOM (Windows PowerShell 5.1 mis-decodes its non-ASCII output without one), and sponsor.yml lost its last OmniVoice references. |
||
|
|
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.
|
||
|
|
a23e69d014 |
chore: point every repo reference at github.com/debpalash/VoiceStudio (#1394)
The repository was renamed. 724 references across 59 files now point at the new URL — README badges, docs, install guides, the updater's releases API call, CONTRIBUTING, the Colab link and the probe harness. GitHub redirects the old URLs, so nothing was broken in the meantime. Deliberately NOT renamed, because each breaks something on a user's machine: the Tauri bundle identifier (the path to every existing user's data), /usr/lib/omnivoice-studio and the compose container names, and the published Docker image paths. The image path needed a code change to STAY still: docker.yml derived it from github.repository, so the next build would have published to ghcr.io/debpalash/voicestudio while Docker Hub, a hardcoded literal, stayed put — everyone pulling the documented GHCR path would have kept receiving the last pre-rename image forever. It is now pinned, with a test that fails if it ever derives from the repo name again. Also makes the probe's repo-name assertion shape-based: it hardcoded the old name and failed on every PR after the rename while the code it tests worked perfectly. |
||
|
|
9877e7c218 |
fix(ci): bind the preview manifest to its run, not to sibling upload times (#1387)
The nightly preview build had been refusing to publish its own healthy manifest since 2026-08-05 — all four matrix legs green, but the macOS bundles uploaded a few minutes ahead of the slowest versioned artifact, and the freshness check compared the version-less darwin tarballs against their siblings with two minutes of slack. Legs finishing minutes apart is normal, so the comparison itself was wrong, and Preview-channel users quietly stopped getting builds. The tarballs are now tied to the run that produced them: anything uploaded after this run's first job began executing belongs to it. A concurrency group serializes preview runs so that holds, and the anchor is the earliest job start rather than the run's created_at (which is stamped while a run is still queued, and would let a queued run claim the previous run's uploads). The preview-notes job also gains the actions: read scope its run-metadata lookup needs, with a warning-and-degrade path so a permissions regression cannot take the channel down again. Regression tests cover the 2026-08-05 shape, the genuinely stale case, clock skew at the boundary, the no-timestamp fallback, per-ref concurrency scoping, and the required permission. |
||
|
|
eeffe6c2d1 |
fix(gguf): a source-built runtime must actually run (#1348) (#1384)
A meticulous report from an LXC/CPU-only source install surfaced three real defects: the build script deleted the libggml shared libraries a dynamically-linked build needs (first spawn died with exit 127), the hardcoded 120s per-spawn kill switch reaped legitimate CPU-only renders, and OMNIVOICE_ALLOWED_ORIGINS — the only fix for cross-origin browser access — was documented nowhere. All platform branches of scripts/build-omnivoice-tts.sh now copy the shared libs next to the binary, the CI artifact glob uploads them, and the backend puts bin/ on the loader path for every spawn of the engine binary. The timeout defaults to 600s (above the pool guard's well-diagnosed 300s deadline), is tunable via OMNIVOICE_GGUF_GENERATE_TIMEOUT_S with non-finite values rejected, and the timeout error names the knob. CORS documented in api-auth.md with a pointer from remote-gpu.md. Regression tests pin the spawn-env rule, the per-branch copy rule, the artifact glob, and the timeout behavior. |
||
|
|
c117bca09e |
fix(release): rebuild + cryptographically verify the preview updater manifest (#1327) (#1362)
* fix(release): rebuild + cryptographically verify the preview updater manifest Since ~2026-07-13 every nightly matrix leg logs 'Signature not found for the updater JSON. Skipping upload...' - tauri-action uploads the bundles and .sig companions but never refreshes latest.json. Combined with the 'Clear this arch's stale preview updater bundle' step (which deletes and replaces the version-less macOS tar.gz every night), the preview manifest's darwin signatures no longer match the published files: macOS Preview users hit 'The signature verification failed' on every update (latest.json frozen at 2026-07-13, tar.gz replaced nightly). Two changes, both in the single post-matrix preview-notes job (no per-leg race): 1. Rebuild latest.json from the release's real assets and their .sig companions, then clobber-upload. The manifest can no longer drift from the files it describes, regardless of what tauri-action's own updater-JSON path does or skips. 2. Extend the existing manifest verification with a cryptographic check: every signature in latest.json must verify (minisign file sig + trusted-comment sig) against the artifact it points at, using the updater pubkey from tauri.conf.json. Parity and version format both passed for 2+ weeks while every darwin entry was unverifiable - this is the check that was missing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(release): refuse a preview manifest built from two different runs Bot review findings on this branch, all fixed here: - The AppImage and MSI were picked independently by highest run number and the larger N became *the* version, so a matrix where one leg failed or was re-run published a manifest advertising X.Y.Z-5 while handing Windows users the -4 MSI. That is the same manifest/artifact drift this job exists to end, reintroduced by the fix for it. Require both legs to come from one run and fail loudly otherwise: leaving the previous manifest in place is a visible, already-understood state; shipping a mismatched one is not. The darwin tarballs carry no run number, so the signature check in the following step is what pins those to the published bytes. - persist-credentials: false on the checkout — nothing here pushes to git. - Floor-pin the cryptography install; this step decides whether a signed manifest is trustworthy, so it is the one dependency worth a bound. tests/test_release_preview_manifest_rebuild.py runs the step body extracted from release.yml against stubbed gh, so it cannot drift from the workflow. Fails before / passes after on the mismatch case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(changelog): note the preview updater manifest fix (#1327) Co-Authored-By: Pinkers01 <pinky.bouw@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci(release): verify the preview manifest before publishing it, not after Two more review findings on this branch, both valid, both about the manifest being wrong in a way the existing checks structurally cannot see. greptile P1 — verification ran AFTER the clobber-upload. A manifest that failed the check was already live and stayed served; the job merely went red, and every macOS Preview user stayed broken until someone noticed. Verification now runs against the file about to be published, and the upload is the last thing in the step. A refusal leaves the previous manifest in place, which is a visible, already-understood state. CodeRabbit — the darwin entries were not tied to this run. The version comes from the AppImage name; the macOS tarballs were only checked for existence. Signature verification cannot help there, because a stale tarball and its stale .sig match each other perfectly — so a run whose macOS legs never uploaded would advertise this version while serving Mac users the previous build, and since those clients keep reporting the old version the updater would re-offer it forever. They are now bound by upload time, with two minutes of slack for legs that finish apart. The selection rules move out of the YAML heredoc into scripts/build_preview_manifest.py. Three findings in a row have been about WHICH artifacts may be described together, and a heredoc can only be tested by extracting it and stubbing a shell — which is what the previous test file did, asserting against gh stubs rather than against the rules. build_manifest is pure: assets in, manifest out, ManifestRefused on anything it will not describe. 14 tests, including both new refusals and two that pin the workflow still calls the module and still uploads last — an inline copy would pass every other test and ship the original bug. Co-Authored-By: Pinkers01 <pinky.bouw@gmail.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Pinkers01 <pinky.bouw@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
8f9f778307 |
fix(scripts): desktop-prod:run wiped the data it was documented to preserve (#1333) (#1339)
* fix(scripts): desktop-prod:run wiped the data it was documented to preserve (#1333) `scripts/desktop-prod.sh` emulates a first install, so wiping is its default: it removes the app data dir, `~/.omnivoice` (the SQLite database, every voice profile, all outputs), the Tauri logs and the WebKit profile. `--keep-data` is the only thing that suppresses that block. `--skip-build` is an independent flag that only skips the cargo compile, and `desktop-prod:run` passed it alone — while the script's own header calls that command "re-launch last build (skip compile)" and its closing banner tells you to use it that way. So "just start it again without recompiling" silently deleted the developer's voice profiles and project database, every time. The fix is in the package scripts rather than the flag parsing: making --skip-build imply --keep-data would remove a legitimate combination (fresh data without paying for a recompile). The two stay independent, and the help text now says so. desktop-fresh:run is deliberately untouched — that script is a stricter new-user emulation, so wiping is the point of its name. Tests pin all three rules, and were confirmed fail-before by reverting the desktop-prod:run line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): kill the live instance before every launch, not only before a wipe Greptile P1 on this PR, and it was a regression I introduced. The app registers tauri_plugin_single_instance, and that callback ignores the incoming argv — it just refocuses the window the RUNNING process already owns. So starting a second copy over a live one does nothing visible. That was previously masked: kill_running_instances sat inside the `KEEP_DATA = false` branch, so every run happened to kill first *because* every run wiped. Adding --keep-data to the re-launch aliases removed the wipe and would have taken the kill with it — `desktop-prod:run:pill` would have left the user in studio mode with --pill silently discarded, and plain `desktop-prod:run` would have refocused the OLD build instead of the one just compiled, which is the entire point of that command. The kill is now unconditional, before the wipe branch. Its two reasons are independent — zombie-backend-after-wipe, and single-instance-swallows-argv — and only the first was ever about wiping. Adjusted its closing line, which said "safe to wipe" and now also runs when nothing is being wiped. New test asserts the call is not nested inside the KEEP_DATA branch; confirmed fail-before by moving it back. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(scripts): scope the kill to this checkout, warn about an installed app Making kill_running_instances unconditional (so --keep-data re-launches still get past single-instance) widened the blast radius of its pgrep: "OmniVoice Studio.app" also matches an installed /Applications copy, so desktop-prod:run would kill the shipped app a developer was using and take their unsaved work with it. That was previously masked — the kill only ran on wipe runs, where a clean slate had been asked for explicitly. Scope the pattern to ${TAURI_DIR}/target/debug/, which covers both launch shapes and nothing else. An installed instance still gets named rather than ignored: single-instance keys on the bundle id, so it swallows this launch too, and silence would just trade one confusing failure for another. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
a3ac1be12d |
fix(ui): report action can't fail silently; correct the Linux data dir (#1177)
CodeRabbit findings 2 and 3 (finding 1, the LAST_FAILURE test race, was already
fixed in
|
||
|
|
aab138ea57 |
fix(backend): surface the shell's start-failure diagnosis instead of "can't reach the backend" (#1177)
The reporter's string is apiFetch's LAST fallback, reached only when no crash
marker exists AND the shell's lifecycle stage is 'failed' or 'unknown'. The
'failed' half was the bug: `BootstrapStage::Failed { message }` carries the
whole diagnosis — exit code plus a ~30-line stderr tail, or the precise reason
`ensure_venv_ready` refused (Intel Mac, a failed `uv sync`, a blocked GitHub) —
and `backendLifecycleStage()` returned only the stage tag, throwing the message
away. Every backend-start failure mode collapsed into one generic, evidence-free
sentence that was also factually wrong: it is not starting, and it will not
recover on its own.
- backendLifecycleStage() returns `{ stage, message }`; a `failed` stage gets
its own branch in apiFetch that surfaces the shell's diagnosis, scrubbed.
- BackendStartFailureNotice renders it after the splash is gone, reusing the
splash's `detectHints` matcher (shared, so the two can't drift) and the
existing bug-report affordance. No Retry advice for unrecoverable failures.
- Rust retains the last `Failed { message }` past a later stage transition
(Retry sets Checking, the supervisor sets StartingBackend) so a respawn can't
erase the first diagnosis; exposed as `last_bootstrap_failure`.
- `bun desktop` prints the exit code and where to look instead of exiting
silently — the from-source twin of the same class ("builds but won't launch").
- Scrub primitives extracted to utils/scrub.js so the transport layer can scrub
without a bugReport -> client import cycle.
Non-Tauri deployments are untouched: there is no shell to fail this way, so the
stage stays 'unknown' and #1164's deployment-specific message still stands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
939b4d4b78 |
test+dev: production-bundle black-screen gate, and stop dev instances colliding
Two independent causes of a blank app window. Never SHIP a blank (the prod hole). v0.3.22 shipped a black screen: a minifier temporal-dead-zone reorder threw before React mounted, leaving an empty #root (#1178). It reached users because every existing check — vitest, node:test, and the whole Playwright e2e suite — runs the UN-MINIFIED dev server, so a bug living only in the minified bundle passes them all. playwright.prod.config.ts + e2e-prod/ close that hole: build the real bundle, serve dist/ via vite preview, and assert the app actually mounts (#root has children, renders visible text, no pageerror). The core assertion is deliberately structural — "did anything mount?" — because that is what a pre-render crash always breaks, whatever its cause. retries: 0, so a blank screen can never be flaky-passed away. Never DISPLAY a blank in dev (the collision). Running `bun desktop` while one is already up does not fail politely, it cascades into a blank window. Reproduced deterministically and measured over CDP: the healthy main window has #root childElementCount 1; after a second launch it is 0. The new launch's port grab makes the running instance's dev:api exit, and `concurrently --kill-others-on-fail` then tears down that instance's whole stack including its Vite server — leaving its window open, pointed at a dev URL that no longer answers. desktop-dev.mjs now clears a leftover dev app first, loudly. The safety boundary for that cleanup is `isDevAppProcess` in desktop-common.mjs: it matches the cargo dev binary (`omnivoice-studio`) ONLY, never the installed release app (`OmniVoice Studio`) — killing a user's real app would be far worse than the bug being fixed. Unit-tested both ways. Also makes the gate runnable off Linux: the dev e2e config hardcodes /usr/bin/chromium, which doesn't exist on Windows/macOS. The new config falls back to Playwright's own browser so a contributor can run the gate before a release. The ci.yml step that runs this gate is NOT in this commit — pushing workflow changes needs a token scope this session lacks. It is provided separately for the maintainer to apply. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
175989241d |
fix(setup): force UTF-8 output so setup.py doesn't crash on piped stdout (Windows)
`scripts/setup.py` prints ✓/⚙ status glyphs. When its stdout is a pipe rather than an interactive console — which is exactly the case under `bun run setup:api` and in CI — Windows Python encodes with the cp1252 codepage, which can't represent those characters, so the script dies with UnicodeEncodeError mid-setup and takes `bun desktop` down at the setup:api step. It only "works" interactively by luck of the console encoding. Reconfigure sys.stdout/sys.stderr to UTF-8 (errors="replace") at startup so the output is identical whether run interactively or piped. No-op where the streams already speak UTF-8 (macOS/Linux, modern Windows Terminal) or can't be reconfigured. Verified: `bun desktop` from a stale terminal now completes setup:api with piped stdout and launches the app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f4fe0844ba |
fix(dev): self-heal cargo PATH so bun desktop works from a stale terminal (#1180)
* fix(dev): self-heal cargo PATH so `bun desktop` works from a stale terminal `tauri dev` shells out to cargo, so `bun desktop` died with "failed to run 'cargo metadata' ... program not found" on any terminal opened before rustup was installed — the shell holds a stale PATH snapshot without ~/.cargo/bin even though cargo is installed and on the persisted User PATH (a new terminal finds it). That's a confusing first-run-from-source papercut, hit repeatedly on Windows. The frontend `desktop` script now runs through scripts/desktop-dev.mjs, which prepends ~/.cargo/bin when cargo isn't already resolvable, then launches `tauri dev` with that healed env. Cross-platform (~/.cargo/bin everywhere), a no-op when cargo is already on PATH, and it passes an explicit env with the correct-case Path key (Bun doesn't propagate process.env mutations to children, and Windows uses "Path" not "PATH"). If Rust isn't installed at all, it prints an actionable install hint instead of the cryptic cargo error. Verified E2E: from a cargo-less PATH, `tauri dev` now compiles instead of failing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note the bun desktop cargo-PATH self-heal (#1180) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: debpalash <tapudattaht@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
08e19e0c1c |
feat(uninstall): opt-in, content-free app_uninstalled ping in the uninstall scripts
Before deleting anything, uninstall.sh / uninstall.ps1 now send ONE
best-effort `app_uninstalled` event — but only when the user opted in:
consent is read from the same prefs.json the app writes, AND the ping needs
the backend-written analytics_info.json (present only while analytics is
enabled — consent + build token — and removed on opt-out), because the
generic scripts ship no token of their own. Payload is content-free: app
version, OS name, random per-install id. 2-second timeout, silent failure,
one honest console line ('Sending anonymous uninstall ping (you opted in to
analytics).'); not opted in => nothing sent, nothing printed, and the
dry-run never sends either way.
Tested by exercising the bash script for real (fake $HOME + a curl shim
recording argv: consented/not-consented/dry-run/missing-info flows, plus
data-still-deleted-after-ping) and by static contract checks on the ps1
(consent gate, TimeoutSec 2, try/catch, no baked token). docs/install/
uninstall.md documents the behavior (docs-sync).
|
||
|
|
e6b1179001 |
feat(dev): loud backend exit banner for bun run dev + docs + changelog (#1164)
In dev there is no supervisor: concurrently's --kill-others-on-fail tears the whole stack down the moment uvicorn exits, the cause scrolls away with the terminal, and the browser tab just says it can't reach the backend — which is exactly how #1164 arrived with zero diagnostics. - scripts/dev-backend.mjs: dev:api now runs uvicorn through a wrapper (command args byte-identical, stdio inherited). On a non-Ctrl+C, non-zero exit it prints a boxed banner: exit code/signal, the last 20 lines of omnivoice.log (data dir resolved exactly like backend/core/config.py), an OOM hint (SIGKILL/137 + the Linux journalctl -k check), and a pointer to the crash notice the run sentinel raises on the next backend start. Exits with the child's own code so --kill-others-on-fail still works. Verified live: started the dev backend, SIGKILLed it, banner printed with the real log tail and exit code 137. - docs-sync: troubleshooting.md gains §14c (browser/dev/Docker crash forensics: the mode-aware error, the dev banner, run_sentinel.json / last_run_crash.json / GET /system/last-run-crash, cap+ack+version-gate semantics) and §14's crash-notice blockquote no longer implies the notice is desktop-only; CONTRIBUTING.md documents the dev:api wrapper. - CHANGELOG.md: [Unreleased] entry for the #1164 class fix. Tests: tests/frontend/devBackend.test.mjs (5) — the uvicorn args are pinned byte-identical, data-dir resolution mirrors config.py, tail/banner content incl. the OOM shapes. |
||
|
|
bec916c348 |
perf(bench): a memory-safe profiler for the pipeline — so "make it faster" stops being a guess (#1129)
Every performance question this week ("can we batch by cores?", "why is dubbing
slow?") was answerable only by measuring, and twice the intuitive answer was wrong:
* Concurrency on Apple Silicon buys NOTHING. Measured, 4 segments:
1 worker 19.3s | 2 workers 20.7s (0.93x) | 3 workers 19.2s (1.00x)
One GPU, already saturated — extra workers interleave. Scaling the GPU pool by
free RAM (the "intelligent batching" that sounds obviously right) would have
added OOM risk on a 16 GB box for zero throughput. _pick_gpu_workers()'s
hardcoded `MPS -> 1` is correct, and now provably so.
* The clone-prompt cache misses on every segment (a dub writes one reference per
segment: 166 distinct keys, cache can never hit). That looked like the dub's
hidden cost. It is 0.40s/segment — ~2% — and it is not even waste: each
reference is genuinely different audio, and encoding it is the *feature*
(per-line prosody). Dropping to per-speaker refs would save ~65s/dub and cost
quality. Not a free win; not taken.
What actually dominates is TTS itself, which scales with text length (3.2s for a
short line, 8.7s for a 2.5x longer one) and is GPU-bound on a GPU that one
inference already fills.
The profiler is deliberately gentle with memory, because a profiler that OOMs the
machine reproduces the very bug class it exists to fix (#1119): stages run one at a
time, models are unloaded between them, a stage is SKIPPED if free RAM is under the
floor rather than starting a load the OS would kill, and each measurement is a fixed
small number of passes — no looping to convergence.
Co-authored-by: mergetest <nizam4103@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4e5d795832 |
fix(uninstall,storage): remove the saved-env leftover; count sidecar engines in disk usage (#1108)
Two recon findings from the reset work, fixed properly (whole class + tests +
docs), plus the destructive reset path is now exercised end-to-end.
1. ~/.config/omnivoice/env survived every uninstall. The app persists the
model-cache location (and a possible HF_TOKEN) there via
backend/core/user_env.py, but the in-app "Remove all data" (uninstall.rs),
uninstall.sh, and uninstall.ps1 all walked past it — so a reinstall silently
inherited the old file and redirected downloads to a maybe-deleted location.
All three now remove it. It's the same expanduser("~/.config/omnivoice/env")
path on every OS, so the Windows script uses %USERPROFILE%\.config\omnivoice.
is_recognizably_ours accepts it (contains "omnivoice"); docs tables updated.
2. Disk usage measured the wrong engines dir. storage_report.default_engines_dir()
returned backend/engines (built-in engine *modules*, no venvs), while sidecar
installs live in DATA_DIR/engines/<id>. So a multi-GB IndexTTS-2 install was
invisible in the engine-venv category and rolled into data/"other". Now points
at DATA_DIR/engines and sizes the WHOLE install (venv + checkout + weights),
with the data category claiming that subtree so it isn't double-counted.
Reset hardening: extracted purge_scopes() as a pure fs function (no AppHandle),
so the actual delete loop runs in tests against a real on-disk install tree —
"everything" wipes the install but spares the venv/foreign temp/sibling folders,
a settings reset keeps content+config+models, and a poisoned data_dir="$HOME"
deletes NOTHING. This is the live drive-through of the destructive path, minus
the GUI.
Also: gitignore the node_modules symlink form (the directory rule node_modules/
never matched a worktree symlink, so it kept slipping into commits).
Tests: Rust 78 (6 new), storage_report 20 (2 new incl. once-not-twice count +
default-dir guard), frontend 1207, i18n probe green, format+lint clean.
Co-authored-by: mergetest <nizam4103@gmail.com>
|
||
|
|
0421be966e |
feat(settings): in-app uninstall — Settings → Storage → Remove all data (#1089) (#1099)
* feat(settings): in-app uninstall — Settings → Storage → "Remove all data" The v0.3.19 uninstaller was a SCRIPT, which never reaches the people who need it: anyone who installed the .dmg / .msi / AppImage has no repo to run scripts/uninstall.sh from — exactly the reporter in #1089, an AppImage user. "Where is uninstall in the app?" had no answer. Now it does. New Tauri commands (uninstall.rs): - uninstall_scan — every folder this install owns, with real sizes, resolved through the same setup.rs helpers the app itself uses, so custom + portable locations are cleaned instead of the defaults being assumed. - uninstall_purge — stops the backend (marking the kill intentional so the #567 supervisor doesn't respawn one into the directories being deleted), removes the folders, and lets the UI quit the app: the Python env it runs on is gone, so there is nothing to return to. This lives in the Rust shell, not the backend, because the biggest thing to remove is the managed Python environment and the backend is RUNNING FROM IT — a process can't delete its own interpreter (and Windows locks the files). Safety: every path must pass is_recognizably_ours() before any remove_dir_all — absolute, not `/` or $HOME, and carrying an OmniVoice-owned component (unit tested both ways). The shared Hugging Face cache is reported separately and is OPT-IN behind its own checkbox with the caveat spelled out: it's the standard HF cache other ML tools share, so sweeping it up silently would delete models this app never downloaded. Deleting voices/projects is irreversible, so the confirm requires TYPING the word, not just a click. Also fixes a real bug in what shipped in v0.3.19: the scripts and docs missed where the BACKEND writes its logs — ~/.local/state/OmniVoice on Linux and %LOCALAPPDATA%\OmniVoice\Logs on Windows (backend_log_path(), backend.rs) — so every Linux/Windows uninstall left a stray log dir behind. Covered now in the scripts, the docs, and the in-app scan. And the scripts now ship as release assets, so cleanup is possible without launching the app at all. Rust: 2 new guard tests. Frontend: 6 new tests (the size on the confirm button must equal what actually gets deleted); suite 1182 passed. Docs synced. Refs #1089 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): drop token border utilities from UninstallPanel (design guard) tests/test_no_literal_borders.py::test_no_token_border_utilities_in_jsx is a backend guard that scans JSX — so a frontend-only test run misses it. It forbids `border-[var(--chrome-border)]` structural utilities: the app-wide border removal converted every panel/row frame away from them, and they render a stray hairline the moment the token doesn't resolve transparent. Row dividers → spacing + an alternating `--chrome-hover-bg` tint; the opt-in checkbox card → a background tint; the confirm input → the sanctioned arbitrary `[border:1px_solid_var(--chrome-border)]` property form the other settings inputs already use (explicitly not flagged by the guard). Guard green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
b1a7ddc374 |
feat(install): clean uninstaller + a straight answer to "where is my data?" (#1097)
A Linux AppImage user asked which folders to delete to remove OmniVoice and whether an uninstaller exists (#1089) — they had to guess. They shouldn't have to: the app is fully local, so uninstalling IS just deleting the folders it wrote, and we never documented them. - scripts/uninstall.sh (macOS/Linux) + scripts/uninstall.ps1 (Windows): find every OmniVoice folder — app data, the multi-GB managed Python env, config, logs — plus, listed SEPARATELY because it is a shared cache, the Hugging Face model cache. Print each with its size as a DRY RUN and stop; delete only on --yes (--models / -Models to include the shared cache). They honor the same env overrides the app reads (OMNIVOICE_DATA_DIR, OMNIVOICE_CACHE_DIR, HF_HOME, HF_HUB_CACHE), and never touch the app binary or anything outside the paths they list. - docs/install/uninstall.md: the complete per-platform path table (what each folder holds and how big it is), the shared-HF-cache caveat, custom/portable locations, per-platform steps to remove the app itself, and what to keep if you plan to reinstall. - Linked from the README FAQ, SUPPORT.md, and install troubleshooting. Paths mirror backend/core/config.py + frontend/src-tauri/src/setup.rs. Verified on macOS: dry-run lists the real dirs; sandboxed HOME runs confirm --yes removes app folders while KEEPING the shared cache, --models removes it, and the env overrides retarget correctly. Closes #1089 Co-authored-by: mergetest <nizam4103@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bd85bab624 |
chore: retire finished planning archives from the repo root (#1095)
Removes ~110 files of process noise (all preserved in git history): .planning/ (GSD-era phases/quick-plans/issue-clusters; workflow retired 2026-07-08), specs/ (spec-kit specs for shipped features 001-007), design/ (pre-React ASCII mockups), research/ (legacy Gradio archive), and .agents/ (rules for a third-party agent tool no longer in use). The four load-bearing decision docs move to docs/adr/ with an archival note; every live pointer follows (gguf engine module docs + quant_map, inject-apprun.sh, pyproject/test comments, fixture README + its seed script — kept byte-identical). The CJK allowlist drops the deleted legacy_gradio entries; STRUCTURE.md and ROADMAP.md document the removal instead of linking into it. Backend suite: 2891 passed. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d3e88c0f3e |
fix(scripts): desktop-fresh kill guard referenced the wrong dry-run flag (#1078)
The kill-before-wipe block used DRY_RUN; the script's flag is dryRun — any run with a live instance crashed with ReferenceError before wiping. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
fb0508f4f6 |
fix(shell): deep health probe before attaching to a running backend; scripts kill before wiping (#1077)
A backend that keeps running while its install is deleted or replaced underneath it still answers /health and /system/info from memory — the launcher's version check passed and the UI attached to a process that 500s every DB-touching route (raw errors without CORS headers, so the webview reports access-control failures). The attach path now requires a DB-touching probe (/profiles) to return an actual 200 status line, and replaces the squatter otherwise — the status line is parsed explicitly because the raw HTTP helper previously returned 500 bodies as Ok. desktop-prod/desktop-fresh now terminate our own running processes (bundle, dev binary, app-scoped port-3900 listener) before wiping, which is how the zombie was produced. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
34c8a33628 |
fix(scripts): desktop-prod builds clean, desktop-fresh emulates a brand-new machine (#1070)
desktop-prod fixes:
- `tauri build --debug` used to produce every bundle and THEN exit 1 at the
updater-artifact signing step (no TAURI_SIGNING_PRIVATE_KEY on dev
machines); the script papered over it with a blanket "non-fatal bundle
error" grep that also swallowed real bundling failures. Local emulation
builds now pass `--config '{"bundle":{"createUpdaterArtifacts":false}}'`
and only build the bundle the script launches (--bundles app / appimage,
--no-bundle on Windows), so the build exits 0. Any nonzero exit now FAILS
the script — the sole tolerated case is a specifically-detected
linuxdeploy/FUSE failure on Linux when the raw debug binary was produced.
- The HF cache wipe ran `rm -rf ~/.cache/huggingface` on macOS/Linux — the
SHARED global cache (backend/core/config.py only relocates it on Windows),
deleting models unrelated to OmniVoice. Non-app-scoped cache paths are now
kept with a "models will be reused" notice; FRESH_NUKE_HF=1 opts in.
- Honest clean marks (removed ✓ / already-clean ○ instead of ✗ for success),
`open -n` always (plain `open` focused a stale running instance instead of
launching the freshly built one), stale-AppImage removal on Linux.
New `bun desktop-fresh` (+ desktop-fresh:run), macOS-only with explicit
refusal elsewhere: true new-user emulation.
- Blank slate: everything desktop-prod cleans PLUS the traces that survive a
reinstall + data wipe — ~/Library/WebKit (webview localStorage), Caches,
HTTPStorages*, Preferences plist (+ defaults delete), Saved Application
State. Per-path found/removed/absent status with sizes; --dry-run prints
the full plan without touching anything.
- Dev-machine camouflage: launches by direct exec of the bundle's Mach-O
(which inherits env — `open` hands off to launchd and drops it) with PATH
stripped of /opt/homebrew/{bin,sbin} + /usr/local/bin and HF_TOKEN /
HUGGING_FACE_HUB_TOKEN / HF_HOME / HF_HUB_CACHE / HF_ENDPOINT /
OMNIVOICE_* unset, and prints a banner of what is hidden.
Shared pure helpers live in scripts/desktop-common.mjs, covered by 9 node
tests (tests/frontend/desktopScripts.test.mjs): every cleanable path is
app-scoped and under $HOME, the PATH/env sanitizers strip exactly the
intended entries, and the build args carry the updater-artifacts-off config.
Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
1808a373a1 |
fix(linux): AppRun workaround detection reads the BUNDLED WebKitGTK version, not the host's (#961 follow-up) (#1024)
The launcher decided whether to export WEBKIT_DISABLE_COMPOSITING_MODE by asking the host's pkg-config — but LD_LIBRARY_PATH makes the BUNDLED libwebkit2gtk the one that actually runs, so on any machine where the two diverge the detection read the wrong number. This was the second bug identified during #961's investigation (the reporter built from source, so their dev packages answered pkg-config with a healthy version while the shipped bundle ran an older lib) and was explicitly deferred in #1007 as not-safely-fixable at runtime. The fix makes it knowable by construction instead: inject-apprun.sh runs at bundle time ON the build host whose libwebkit2gtk gets bundled, so it stamps that version into .bundled-webkitgtk-version inside the AppDir. AppRun reads the stamp first and only falls back to host pkg-config for bundles predating it. Empty/unreadable stamp fails safe (workaround on), same philosophy as the missing-pkg-config path. Tests: 3 new cases in AppRun.test.sh — marker-beats-host in both directions (broken-marker/healthy-host and the #961 inversion, healthy-marker/broken-host) plus empty-marker fail-safe. Also wires AppRun.test.sh into pytest (tests/test_apprun_launcher.py) — it was previously run by NO CI job, so the launcher could regress silently. Also documents Windows install-to-another-drive behavior in docs/install/windows.md (#938): local drives work via the wizard's directory picker, mapped network drives are a Windows Installer limitation, and the data directory moves independently of the app. 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> |
||
|
|
72d137e1f3 |
fix(bootstrap): port cuDNN 8 (NVIDIA CUDA GPU) + VC++ redist to packaged installs (#869)
* fix(bootstrap): port cuDNN 8 (NVIDIA CUDA GPU) + VC++ redist install into ensure_venv_ready() * fix(bootstrap): address #869 review — drop dead VC++ half, cache negative CUDA probe, gate on ROCm, sync docs Per maintainer review on #869: 1. Drop the VC++ Redistributable half: LoadLibraryA("vcruntime140.dll") from the running Tauri exe is a tautology (the exe itself links the MSVC CRT, so the process wouldn't be running without it), and torch's real failure mode is msvcp140.dll inside the venv python process. Dead code removed; a comment records why for future readers. 2. Stop taxing every non-CUDA launch: a negative torch probe (CPU / Intel / AMD — most installs) is now cached in a .venv/.cudnn8_probe_negative marker, so the synchronous `import torch` runs at most once per venv lifetime. Invalidated on every path that can change the torch build (drift sync #307, repair sync, first-run sync, ROCm reinstall) and implicitly by a venv rebuild. A probe that fails to run cleanly is skipped WITHOUT caching so a transient error can't wedge a real CUDA machine. 3. Rewrite docs/install/troubleshooting.md §10 to the actual root cause: packaged installs never had the cudnn8_compat libs (so reinstalling never restored them); the bootstrap now installs them automatically on CUDA machines, with the manual uv pip command as the offline fallback and PyTorch Whisper as the sidestep. 4. Gate the ~700 MB nvidia-cudnn-cu12 download on the venv torch being a real CUDA build: the probe now reports 'hip' before checking cuda.is_available() (which HIP spoofs), so opt-in ROCm installs (#124) never fetch the CUDA wheel. Also reflow the CHANGELOG entry to house style (bold one-line lead, 1-3 lines of why, (#827, #869) refs) and extend the bootstrap unit tests: classify_cuda_probe verdict mapping and the marker write/invalidate round-trip (6 cuDNN tests total, 43 lib tests green). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: mergetest <test@local> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c44bae0d0d |
feat(ui): migrate waveform-* to utilities, trim index.css (P4) (#816)
Move the waveform-* global class family off index.css onto Tailwind v4 utilities on WaveformTimeline.jsx, then delete the now-dead rules. Migrated to utilities (rules deleted): waveform-timeline (mb), waveform-controls + -left/-right (flex/items/justify/gap), waveform-btn + :hover/:disabled and waveform-btn-play + :hover (shared WF_BTN/WF_BTN_PLAY consts; UA <button> padding/font preserved since the app ships no preflight), waveform-time (text/border/bg/mono/tabular-nums), waveform-zoom-slider (important w/h/mt). States -> hover:/disabled: variants; no-preflight borders -> explicit [border:1px_solid_...]; exact px via arbitrary values. Deleted as dead (zero usages anywhere): waveform-video-preview, waveform-track-bg (+ nth-child + the 800px media-query track rows). Kept (irreducible): .waveform-container and its .waveform-container [data-id^="wavesurfer-region"] descendant rules (+ the 800px container/region media query) — those style WaveSurfer-generated DOM we don't render in JSX, so they can't be utilities. The class stays as a hook. index.css net -55 lines (+7/-62). Cascade-correctness verified live (Playwright getComputedStyle, both stylesheets loaded): new utilities reproduce the pre-migration computed styles exactly. Caught two subtleties: (1) controls margin-top is 3px (unlayered wfm-controls already wins over the old 4px), so no mt utility is added; (2) referencing var(--chrome-font-mono) in a class string tripped the global [class*="chrome-font-mono"] selector (adds slashed-zero + ss02) — switched the time font to var(--font-mono) (identical stack) to avoid the substring match. Screenshot pixel-diff old vs new = 0 (AE). Updated record_promo.js's fallback selector (.waveform-controls -> [aria-label="Playback controls"]). Gates: oxlint 0, oxfmt clean, vite build, vitest 641 pass, test:visual 48 pass, bun install --frozen-lockfile no change. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4eac1b50d5 |
chore(desktop-prod): add --keep-models for fast fresh-app runs (#650)
`bun desktop-prod` (clean) wipes everything including the HF model cache, so every fresh-install emulation re-downloads multi-GB weights — slow and bandwidth -heavy, and the exact pain users on flaky networks hit. --keep-models wipes app/backend data, logs, and webview state for an honest first-run, but KEEPS the model cache so the weights aren't re-pulled. Ignored under --keep-data (which keeps everything). Adds the `desktop-prod:keep-models` convenience script. Scripts-only package.json change — no deps, bun.lock unaffected. Co-authored-by: mergetest <test@local> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
de80856cd9 |
test: backend route-inventory + webUI feature-coverage guards (#609)
* test: backend route-inventory snapshot + webUI feature-coverage guards A reusable testing system that verifies every feature surface is present: - tests/test_api_route_inventory.py: boots the app, diffs all 213 routes vs a committed snapshot (tests/fixtures/api_routes.txt), guards a critical-endpoint set, and floors the route count — any endpoint drift fails CI. - scripts/dump_api_routes.py: regenerates the snapshot. - frontend featureCoverage.test.js: every AppMode has a render branch, every lazy-imported page file exists, every feature has an i18n namespace. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): note the feature-coverage test system * test(api-inventory): isolate via subprocess + exclude env-dependent mounts CI surfaced two flaws in the first cut: - the in-process app import + sys.modules purge polluted later DB-touching tests (a cascade of 404s in test_dub_subtitles_309 etc.); - the snapshot included StaticFiles mounts (/demo_audio) and a conditional GET / root that register based on filesystem state, so a macOS-generated snapshot didn't match a fresh Linux CI runner. Compute routes in an isolated subprocess (scripts/dump_api_routes.py --print) and cover only the deterministic router surface (drop Mounts + root). 209 routes; inventory + previously-polluted tests now pass together. 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> |
||
|
|
e862f0faf0 |
feat(asr): crash-isolated faster-whisper subprocess backend (Wave 4.2) (#393)
* feat(asr): crash-isolated faster-whisper subprocess backend (Wave 4.2) Native ASR engines (faster-whisper / CTranslate2) can segfault on GPU teardown — a process-level crash that kills the whole backend. Running the engine in a child process turns that into a failed job: the sidecar dies, the parent raises a decorated error (engine id + device), and the next request respawns a fresh sidecar. - services/subprocess_asr.py: SubprocessASRBackend reuses SubprocessBackend's wire protocol + lifecycle — including respawn-on-dead-process (_spawn relaunches when the child isn't alive) and GPU-slot acquire/release — adding a 'transcribe' op (the TTS 'generate' surface is stubbed). IsolatedFasterWhisperBackend wraps faster-whisper using the PARENT venv (already a dep — only the process boundary is new); opt-in via OMNIVOICE_ASR_BACKEND=faster-whisper-isolated. - engines/_asr_sidecar/main.py: the faster-whisper runner (stdlib wire protocol; torch/CT2 import lazily so the ready handshake fits the timeout). - engines/_echo/main.py: a 'transcribe' echo op so the round-trip + crash recovery are testable without a real engine. - asr_backend._REGISTRY is now a lazy dict (mirrors the TTS registry) so the isolated backend lists/resolves without importing the subprocess stack unless selected. Tests (echo sidecar, stdlib-only): round-trip, single long-lived sidecar across calls, crash-mid-transcribe → decorated error + backend healthy + next call respawns, registry exposure, generate-not-supported. Spec 7 / parity program Wave 4.2. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(asr): deterministic crash test + drift marker for lazy ASR registry (Wave 4.2 CI) CI surfaced two issues: - The echo crash test relied on the crash-AFTER-reply hook, whose reply may still reach the parent (timing-dependent) — and a leaked OMNIVOICE_ECHO_CRASH from a sibling subprocess test poisoned the non-crash tests. Fix: a deterministic OMNIVOICE_ECHO_CRASH_NO_REPLY hook that exits BEFORE replying (guaranteed dead pipe → decorated error), and the asr fixture clears both crash envs so the round-trip/two-call tests can't inherit a leak. - check-docs-drift's _ASR_MARKER didn't match the new lazy registry line (_LazyASRRegistry({); updated the marker + the self-test fixture. Verified the no-reply crash hook by driving the sidecar directly (reply=None, exit 1); drift self-test + real-repo check green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(asr): allowlist the 'segments' op so transcribe replies aren't dropped (Wave 4.2 CI) The parent's PARENT_INBOUND_OPS frozenset gated inbound sidecar frames but never included 'segments' — the ASR transcribe reply op. _recv() dropped the frame as disallowed, tail-recursed, hit EOF, and returned None, so every transcribe surfaced as a bogus 'sidecar crashed mid-transcription'. TTS ('audio') was allowlisted; ASR ('segments') was missed. Add it (and list 'transcribe' in the informational SIDECAR_INBOUND_OPS), update the exact-shape allowlist test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
11c498eeb5 |
ci(docs): daily docs-drift job — canonical inventory vs README/docs/registries (Wave 0.1) (#353)
docs/features.yaml is the curated single source of truth (12 features, 11 TTS + 7 ASR engine ids, required install docs). scripts/check-docs-drift.py diffs it against README.md, docs/, and the engine registries — parsing registry keys from source so the CI runner never imports torch. The daily workflow updates ONE rolling 'docs-drift' issue in place and auto-closes it when clean (pattern adapted from Patter, MIT). Self-test includes a real-repo-is-clean gate, so any PR that changes engines/features without updating the inventory fails CI too. Spec: docs/competitive-analysis.md Spec 9a / parity program Wave 0.1. Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
948bc76543 |
macOS: ad-hoc sign so users open without Terminal + signing/notarization verification (#290)
* chore(release): add macOS signing/Gatekeeper/notarization verification Codify and enforce the macOS build-signing requirements. The release pipeline built bundles and had opt-in Apple signing, but never verified codesign/spctl/notarization — unsigned or broken bundles could ship silently. - scripts/verify-macos-signing.sh: runs codesign --verify --deep --strict, spctl Gatekeeper assessment, per-nested-Mach-O signature check, stapler validate, and (opt-in) notarytool history. Report-only by default (unsigned dev/preview is expected); --require-signed fails on any unsigned/un-notarized component so a broken release stops instead of publishing an unsigned artifact. - scripts/macos-dev-unquarantine.sh: local-dev-only quarantine stripper, with a loud "never a substitute for notarization" warning. - release.yml: new "Verify macOS signing" step on the macOS leg — report-only on unsigned paths, STRICT on the opt-in signed stable path (same condition as "Configure Apple signing"), so signing/notarization failures fail the job. - docs/macos-signing-verification.md: the canonical 10-point requirements + how-to-verify checklist, cross-linked to docs/install/macos.md and DESKTOP_RELEASE.md. Verified locally: report-only PASS (exit 0) and --require-signed FAIL (exit 1) against the real unsigned debug .app; release.yml parses as valid YAML. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(macos): ad-hoc sign bundle so users open it without Terminal (no Apple ID) The "app is damaged and can't be opened" error is caused by a broken/incomplete code-signature seal (codesign --verify failed: "code has no resources but signature indicates they must be present") on the quarantined download — there is no GUI bypass for that variant on modern macOS, forcing users to run `xattr`. Give the bundle a VALID ad-hoc signature at build time (free, no Apple Developer account) via tauri.conf.json bundle.macOS.signingIdentity = "-". Verified through a real `tauri build`: the produced .app is now flags=adhoc,runtime and passes codesign --verify --deep --strict. A valid seal flips the Gatekeeper prompt from the un-bypassable "damaged" to the GUI-bypassable "unidentified developer", which users clear with right-click → Open / Settings → "Open Anyway" — no Terminal. Still not notarized (that needs the paid Apple ID), so there's a one-time confirmation rather than a clean double-click. The opt-in Developer-ID path is unchanged: APPLE_SIGNING_IDENTITY (env) overrides the "-" default on the signed stable release. - tauri.conf.json: signingIdentity "-" (ad-hoc default). - verify-macos-signing.sh: detect ad-hoc tier; report the no-Terminal GUI path in report-only, still FAIL it under --require-signed (production must notarize). - docs/install/macos.md: lead the Gatekeeper section with right-click → Open; keep xattr as fallback for the harsher "damaged"/corrupted-download case. - docs/macos-signing-verification.md: signing-tiers table + ad-hoc default note. - release.yml: comment the ad-hoc default + env override on the signed path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ea26893bfc |
fix(scripts): desktop-prod works from cmd/PowerShell via cross-platform launcher (#282) (#333)
`bun run desktop-prod` (and its :run/:upgrade/:pill/:run:pill variants) invoked `bash scripts/desktop-prod.sh` directly. On Windows, cmd and PowerShell have no `bash` on PATH unless Git Bash happens to be there, so the documented from-source install path died with a cryptic spawn failure before printing anything — the exact first step in issue #282's repro. Add scripts/desktop-prod.mjs, a tiny launcher (runs under bun or node): - macOS/Linux: execs the bash script unchanged — zero behavior change. - Windows: locates Git Bash via `where.exe bash`, well-known Git for Windows install paths, or derived from git.exe's location; explicitly skips C:\Windows\System32\bash.exe (the WSL launcher, which would run the script inside Linux and wipe/launch the wrong paths). - No usable bash: prints an actionable error (install Git for Windows, use `bun run desktop`, or use the installer) instead of a spawn error. All flags are forwarded untouched and the child's exit code is propagated. scripts/desktop-prod.sh itself is unchanged, and docs/install/windows.md now lists Git for Windows as a prerequisite for from-source installs. Refs #282 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f555fe0720 |
fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253) (#254)
* fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253) Three gaps flagged by review bots on PR #253 are addressed: 1. **Layer-2 repair result captured** (`bootstrap.rs` ~line 481): the `let _ = run_streaming(...)` that silently discarded network/permission failures from the targeted `uv pip install setuptools>=75,<80` is replaced with a `match` block that logs `log::info!` on success and `log::error!` on failure (consistent with the Layer-3 path). 2. **Post-repair re-verification added** (`bootstrap.rs`): after the targeted install in Layer 2, a second `import pkg_resources` check is run. If pkg_resources is *still* absent, a `log::error!` with an actionable remediation message is emitted before returning. This closes the gap where bootstrap handed back a known-bad venv that caused the dubbing crash (#248) with no clear signal in the log. 3. **Test strengthened** (`bootstrap.rs` `setuptools_repair_uses_correct_specifier`): the test now mirrors the exact `&[&str]` slice used in both repair branches and asserts `repair_args[2] == "setuptools>=75,<80"` as a single positional argument. This catches the split-arg regression the review bot identified (e.g. `["setuptools>=75", ",<80"]`) which would silently install the latest setuptools and leave pkg_resources absent. 4. **Smoke-test INST-01/02 hardened** (`scripts/smoke-test.sh`): exports `UV_PYTHON_PREFERENCE=only-system`, `UV_HTTP_TIMEOUT=120`, and `UV_HTTP_RETRIES=5` before the `uv run` import checks so that failures reflect real bootstrap regressions, not harness-network timeouts. `cargo test bootstrap` → 5 passed, 0 failed. Closes review findings on #253. Related: #248. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(bootstrap): fail clearly when pkg_resources repair fails (PR #254 review) - ensure_venv_ready now returns None (via fail()) when pkg_resources is still missing after the targeted setuptools repair, instead of returning a venv that crashes on the first ASR/dub call. The 'pkg_resources' message routes to the PKG_RESOURCES_MISSING failure mapping for a clear, doc-linked remediation. - smoke-test.sh: correct the comment (timeout+retry vars, not a non-existent index var). --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c98b0aaf6f |
fix(bootstrap): guarantee pkg_resources in backend venv (closes #248) (#253)
Root cause: the existing-venv fast-path in ensure_venv_ready() only
checks `import uvicorn` before returning — it never verified that
`pkg_resources` (dropped by setuptools≥80, issue #224) was present.
Users who installed before commit
|
||
|
|
87e4df95cd |
i18n: backfill update-channel + auto-update keys across 18 languages (#202)
PR #200 added 18 new locale files, but they predated #199 (auto-update badge + Stable/Preview channel toggle), so they were missing the `update.*` namespace (6 keys) and `about.channel_*` (5 keys) — those strings fell back to English in ar/de/es/fr/hi/id/it/ja/ko/nl/pl/pt/ru/sv/th/tr/uk/vi/zh-TW. Backfill all 11 keys in every one of those languages so the updater UI is fully localized. en.json / zh-CN.json already had them and are untouched. Placeholders ({{version}}, {{pct}}, {{channel}}) preserved verbatim; files re-emitted in the exact format scripts/translate_all.py writes (ensure_ascii=False, indent=2) so the diff is additions only (+13 lines/file, 0 deletions). Also fix scripts/translate_all.py: LOCALES_DIR was hardcoded to a contributor's absolute path (/Users/.../orca/...) — make it repo-relative so the generator actually runs for anyone. Verified: all 21 locales valid JSON + key-complete, placeholders intact; tsc clean; vitest 162/162; build OK; CJK guard passes (locales are the allowlisted translation layer). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
52d7321287 |
feat(l10n): complete translations for all 21 languages (837 keys each) (#200)
Translate all UI keys across every component for 20 non-English locales:
ar, de, es, fr, hi, id, it, ja, ko, nl, pl, pt, ru, sv, th, tr, uk, vi, zh-CN, zh-TW
- 837 flattened keys per language (100% coverage)
- Covers settings, splash, main UI, dialogs, tooltips, errors
- Placeholders ({{var}}) and HTML tags (<1>) preserved
- Add translate_all.py batch script for future re-translations
|
||
|
|
e3d7815df0 |
fix(scripts): Windows (Git Bash/MSYS) support across install/run/smoke-test (#164) (#166)
Follow-up to #165 — the same uname Darwin/Linux-only pattern in the other dev scripts: - smoke-test.sh: add MINGW*/MSYS*/CYGWIN* detection + Windows data paths (%APPDATA%/%LOCALAPPDATA%, per backend/core/config.py) + .exe binary suffix. - run.sh: Windows detection + %LOCALAPPDATA% log dir; backend launch already uses 'uv run' which is cross-platform. - install.sh: detect Windows and print a clear 'use the .msi / WSL' message (the brew/apt system-dep installer can't run on native Windows) instead of silently treating it as Linux. build-omnivoice-tts.sh already handled windows-x86_64; record-reference.sh already guards macOS-only with a clear message. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
41176f6ef1 |
fix(scripts): support Windows (Git Bash/MSYS) in desktop-prod.sh (#164) (#165)
uname -s returns MINGW64_NT/MSYS_NT/CYGWIN_NT under Git Bash, which hit the catch-all 'Unsupported platform' error. Add a windows case + a Windows data- path branch (%APPDATA%\OmniVoice backend data + %LOCALAPPDATA% Tauri/HF dirs, per backend/core/config.py) + launch the debug .exe. The cross-platform 'tauri build' step is unchanged; every rm stays [ -d ]-guarded so an off path is a no-op, never a wrong delete. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
8b00dc1f4f |
feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage (#133)
* feat: onboarding demos, opt-in bug reporting, error-docs deeplinks + issue triage Working-tree snapshot bundling several in-flight workstreams (v0.3.0): - Onboarding/demo system: DemoPresetGrid, DictationDemo, DubbingDemo components + tests, render scripts (render_demos_omnivoice.py, build_demos.sh, build_dub_demo.sh), personalities preview URLs, alembic 0002 voice-profile demo fields. - Opt-in bug reporting: ReportBugButton (prefilled GitHub-issue URL path). - Error transparency UX: errorDocsMap deeplinks + BootstrapSplash/error wiring. - Dub workspace: DubSegmentRow/Table, WaveformTimeline, dubSlice tweaks. - Issue triage: .planning/issue-clusters/ (plan-01..05 root-cause masters, GH #128-#132). - CLAUDE.md: hard rule — everything ships on v0.3.0, no version bumps. KNOWN GAP (why this is a draft): the generated demo audio assets are NOT in this tree, and backend/assets/samples/demo_voice.wav is deleted. onboarding.py guards the missing file (skips seeding the demo profile with a warning), so no crash — but first-run Launchpad will be empty and /demo_audio/ preview URLs 404 until assets are regenerated via scripts/build_demos.sh. Do not merge before regenerating + committing the demo assets. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dub): timing strategies — kill audio compression, add Concise + Stretch Video Replaces the current audio time-compression default (atempo squeeze to fit slot) that produced chipmunk/alien output on high-density target languages like Bengali. Two new user-selectable modes; legacy behaviour kept behind an explicit "Strict slot" choice. New `DubRequest.timing_strategy` enum (default "concise"): - "concise" Translator trims text to fit at natural rate; if it still overflows, hard-trim at slot with a fade so we never overlap the next speaker. Surface overflow_s per segment so the user can shorten the text. - "stretch_video" Audio plays at natural 1.0× rate. Backend computes a per-segment new timeline; persists a video_stretch_plan on the job. Mux step (dub_export) builds an ffmpeg trim+setpts+concat filter graph that stretches each segment's video portion to match the natural-rate dub audio. Gaps/pre-roll/tail pass through at 1.0×. Sub burn under stretch_video is skipped in one pass (cues would drift). - "strict_slot" Legacy atempo squeeze. Retained for back-compat. Director rate-bias side-effect (seg_speed *= bias) now gated on strict_slot only, so "urgent"/"slow" direction tokens keep their instruct effect in the new modes without chipmunking. Per-segment fit_status emitted in the SSE done event: {status: "fits" | "overflows" | "video_stretched", overflow_s?, stretch_ratio?} DubSegmentRow's "Sync: 100%" badge (which was lying — sync_ratio was always ~1.0 because the TTS loop pre-trimmed to slot) is replaced with a truthful "Fits / Overflows +Ns / Video 1.18×" label. Frontend: - prefsSlice.timingStrategy (persisted, store v3→v4 with safe migrate). - DubTab footer Segmented control: "Concise · Stretch Video · Strict slot". - useDubWorkflow passes timing_strategy on /dub/generate; consumes fit_status. Tests: tests/test_dub_timing_strategy.py — 13 cases covering schema defaults/validation, _build_video_stretch_filter_graph (pre-roll, gap, tail, empty-plan early return, post-subtitle chain-in), and _video_stretch_plan_for guards. 30/30 existing dub tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(waveform): surface missing source as "Source media missing" instead of code-4 black box When a project's underlying media file is gone (moved or deleted between save and reload) the <video> element fires MediaError code 4 and the companion audio fetch returns HTTP 404 — both were silently warned to the console while the user stared at an unresponsive black panel and an empty waveform. - WaveformTimeline now flips loadError when the video element rejects code 3 (decode) or 4 (src not supported), and tracks `sourceMissing` separately so the error UI can name the actual problem. - The audio decode fallback chain catches HTTP 404 specifically and treats it as source-missing instead of loading silent empty peaks — an empty waveform on a deleted source is more confusing than a clear "Re-upload the video to continue" message. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(tray): "Show OmniVoice" reloads when the webview is blank When the dev Vite server restarts (or the main window is created before the backend is ready), the webview load fails and the window is left with `<body></body>` plus a "Could not connect to the server" console error. Clicking "Show OmniVoice" from the tray menu just re-showed the broken window — there was no recovery path short of quit+relaunch. Now the show handler runs a tiny eval after `show()`/`set_focus()` that calls `location.reload()` only when `document.body.childElementCount === 0`. A healthy window doesn't blink (body is non-empty); a blank one self-recovers as soon as the user clicks Show. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#133): bug-report diagnostics field mapping + drop unused imports Address PR #133 review: - ReportBugButton: /system/info exposes `platform` + `device`, not `os`/`torch_device`/`gpu` — those reads silently dropped OS/GPU from every bug report. Map to the real fields (CodeRabbit). Also remove the dead `home` local in stripHome (CodeQL unused-variable). - DictationDemo: drop unused `Loader` import (CodeQL unused-import). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
898f41a57d |
fix(windows): gate torch.compile on Triton + ASR critical-path smoke (plan-02, closes #65) (#138)
* fix(windows): gate torch.compile on Triton availability (#129, closes #65) plan-02. torch.compile(mode="reduce-overhead") needs Triton at runtime; Triton has no Windows wheel, so the old `device=="cuda"`-only guard in model_manager.py failed on Windows+CUDA and surfaced as a confusing "OOM" (#65). Inference-time, hard to diagnose. - engine_env.should_torch_compile(device): requires CUDA + find_spec("triton") + the existing perf.torch_compile_disabled setting being off; logs the skip reason at INFO and falls back to eager. - model_manager.py call site uses it instead of the bare cuda check. - smoke-test.sh INST-02: import torch + ctranslate2 + whisperx (full ASR path) so a missing transitive dep fails the build instead of crashing mid- transcription (#116). Runs in the CI smoke-matrix on Win/macOS/Linux. setuptools>=75.0 (fix-sequence step 1) already pinned (#58). Linux/CUDA+Triton behaviour unchanged. Tests (TDD): tests/test_torch_compile_gate.py (4). Closes #65; addresses #129/#116. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(windows): also gate subprocess torch.compile on Triton (Greptile #138) Greptile flagged that the in-process gate left a parallel gap: engine subprocesses honour TORCH_COMPILE_DISABLE, but build_engine_env() only set it on the user's Performance toggle — so a Triton-absent host (Windows, or macOS) still exposed subprocess engines to the same crash this PR fixes in-process. - build_engine_env(): set TORCH_COMPILE_DISABLE=1 when the user disabled compile OR Triton is unavailable (find_spec), cross-platform — mirrors should_torch_compile(). Drops the Windows-only scoping (and the now-unused `import sys`). - Refreshed the stale module docstring. - 3 new tests cover the subprocess gate (triton-missing, triton-present, user-opt-out). 7/7 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert(engine_env): keep subprocess TORCH_COMPILE_DISABLE user-driven Reverts the build_engine_env() broadening from the previous commit. Auto- disabling subprocess torch.compile on Triton-absence conflicts with a deliberate, tested contract (test_perf_settings: Windows + flag-off ⇒ no injection; non-Windows ⇒ never inject) — the subprocess var is intentionally under the user's explicit control. The #65 fix is the in-process should_torch_compile() gate (unchanged here), which IS automatic and fully tested. Pushing back on the subprocess auto-gate as a separate, deliberate contract change rather than forcing it through by rewriting established tests. 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> |
||
|
|
0f73a64101 |
fix(desktop-prod): also clean backend data dir for actual fresh-install emulation (#103)
The script was cleaning Tauri's APP_ID dir (~/Library/Application Support/com.debpalash.omnivoice-studio) but the Python backend writes to ~/Library/Application Support/OmniVoice — a separate hardcoded path in backend/core/config.py::get_app_data_dir(). Result: "🧹 Cleaning all OmniVoice data for fresh prod emulation" was deleting an empty directory while the real user data (SQLite db, voice profiles, dub jobs, outputs, logs) sat untouched. Developers running desktop-prod thought they were testing a clean install path, but were actually testing on accumulated state. Fix: add a BACKEND_DATA variable and a 1b cleanup step targeting the backend's actual data dir. Per platform: - macOS: ~/Library/Application Support/OmniVoice - Linux: ~/.omnivoice - Windows: %APPDATA%/OmniVoice (not in this script; Windows uses .bat) Surfaced while running `bun desktop-prod` for the first time today on a clean tree — the .app launched fine but Settings showed a pre-existing voice profile from a prior session, contradicting the "fresh" claim. 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>
|
||
|
|
c32041289d |
Phase 1 Wave 3: AppImage launcher + .deb ffprobe + Docker LAN + Gatekeeper probe (closes #54, #56, #76, #80) (#93)
* fix(appimage): conditional WEBKIT_DISABLE_COMPOSITING_MODE launcher (#56) WebKitGTK 2.44.x and 2.46.x have a compositing-path regression on Wayland that blanks the AppImage's first paint on Fedora 44 / Ubuntu 24.04. Setting WEBKIT_DISABLE_COMPOSITING_MODE=1 forces the software fallback that works, but blindly setting it on healthy WebKit versions (2.48+) regresses those. This wave adds a conditional AppRun launcher that detects the WebKit version via pkg-config and only sets the env var on the broken ranges (plus a fail-safe when pkg-config is absent or the version is unknown). The launcher is injected into Tauri's AppImage staging dir via a beforeBundleCommand hook — see .planning/decisions/apprun-strategy.md for the spike outcome and rationale (Strategy B chosen). Phase 1 Wave 3 — Plan 01-03 Task 1. Closes #56 frontend half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deb): relocate bundled ffprobe out of /usr/bin to avoid conflicts (#76) Prior versions placed the bundled ffprobe at /usr/bin/ffprobe via Tauri's externalBin, which overwrites the system ffprobe on Ubuntu 26.04 and collides with apt-installed media-package ffprobe. Relocate the .deb-bundled ffprobe to /usr/lib/omnivoice-studio/bin/ffprobe via bundle.linux.deb.files, plus defensive maintainer scripts: - preinst: ensure target dir exists for upgrade flows - postinst: remove legacy /usr/bin/ffprobe ONLY when dpkg confirms our package owns it (never touches a user's distro ffprobe) - postrm: clean up the relocated path tree on purge/remove Rust side (tools.rs::resolve_ffprobe) now probes the new path on Linux, and backend spawn (backend.rs) carries both FFPROBE_PATH (legacy alias) and OMNIVOICE_FFPROBE_PATH (canonical) into the backend env. Python side (ffmpeg_utils.resolve_ffprobe) reads OMNIVOICE_FFPROBE_PATH first, falls back to FFPROBE_PATH, then to shutil.which("ffprobe"). 6 new unit tests cover the env-cascade resolution. Phase 1 Wave 3 — Plan 01-03 Task 2. Closes #76. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(frontend): centralised apiBase resolver for Docker LAN access (#80) Docker / LAN browser users hit the preview API at the LAN host's IP, not their local machine — the prior frontend/src/utils/media.js:20 hardcoded http://localhost:3900, which from a LAN client resolved to the client machine itself. Centralise via frontend/src/utils/apiBase.ts: 1. VITE_OMNIVOICE_API override (Docker compose / dev) always wins. 2. Tauri webview → http://localhost:3900 (unchanged behaviour). 3. Plain browser → ${window.location.protocol}//${window.location.hostname}:3900 (follows the page's origin — closes #80). 4. SSR / no-window → http://localhost:3900 (safe fallback). Grep-sweep confirmed media.js:20 was the only hardcode site (Assumption A4 in 01-RESEARCH.md verified). 6 new vitest cases cover the resolver. Phase 1 Wave 3 — Plan 01-03 Task 3. Closes #80 frontend half. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(backend): macOS Gatekeeper quarantine probe + INST-01 guard (#54) Adds backend/core/gatekeeper_detect.py which walks up from sys.executable to find the .app bundle and runs `xattr -l` to check for the quarantine extended attribute (com.apple.quarantine). On detection, the lifespan startup probe logs a structured warning and emits a system_error event through the existing event bus with error_class="GATEKEEPER_QUARANTINE", which Wave 2's React ErrorBoundary turns into a docs deeplink. Detection is informational only — we never auto-run `xattr -cr` (the app itself is quarantined and cannot fix its own state per Anti-Pattern in 01-RESEARCH.md). Users get a clear pointer to the workaround docs. GET /system/quarantine-status exposes the structured payload so the frontend can poll on first load. INST-01 (setuptools>=75.0 pin from PR #62) gains a PR-time guard in tests/backend/test_pyproject.py + a user-observable smoke check in scripts/smoke-test.sh (pkg_resources + whisperx import). 7 gatekeeper tests + 1 pyproject test added — all pass. Phase 1 Wave 3 — Plan 01-03 Task 4. Closes #54 backend half (Wave 2 owns the docs page + ErrorBoundary deeplink wiring). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
1941e3fcc0 |
fix(docker): GPU detection in containers + compose profiles + sonitranslate cuDNN sub-repo (#74)
Docker GPU support hardening + documentation. - Restores docker compose --profile gpu up path; documents NVIDIA Container Toolkit setup in README - Splits CPU vs GPU compose services cleanly (deploy/docker-compose.yml) - backend/api/routers/setup/wizard.py: GPU detection in containerized environments uses torch.cuda fallback - New scripts/setup.py replaces deleted scripts/setup_cudnn.py - New test: tests/test_setup_preflight.py - CHANGELOG.md + README.md updated Complementary to PR #77 (community PYTHONPATH fix) — different sections of docker-compose.yml. |
||
|
|
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> |
||
|
|
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> |
||
|
|
0ecbf136e7 |
refactor: codebase cleanup & root folder reorganization (#38)
refactor: codebase cleanup & root folder reorganization |
||
|
|
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> |