- Implement donate page and migrate API fetching to react-query hooks
- Add setup wizard for batch job management and voice clip editing
- Refactor setup router into package (wizard, models, download sub-modules)
- Fix 9 CI test failures from setup router refactor
- Fix cross-device link error in prefs.py atomic writes
- Fix event loop mismatch in export test fixtures
- Modernize README with architecture diagram and 13 app screenshots
- Defer per-segment disk writes in dub_generate for ~6s faster dubs
- Extract 45 inline styles from Launchpad, KeyboardCheatsheet, DubSegmentRow
- Add playwright dev dep and screenshot capture script
v0.2.2 users upgrading from a PyInstaller build saw "Failed to load
engines: Load failed" because the app window opened before the
first-run `uv sync` (5-10 min) could finish populating the venv. The
webview just hung on a blank state.
Wire the setup through properly:
Rust (src-tauri/src/lib.rs):
- New `BootstrapStage` enum: checking → downloading_uv →
creating_venv → installing_deps → starting_backend → ready (or
failed { message }). `#[serde(tag = "stage")]` so it serialises as
a tagged union the frontend can switch on.
- `BootstrapState` exposed via `bootstrap_status` Tauri command so
React can poll progress.
- `setup()` no longer blocks on `ensure_venv_ready`. Instead spawns a
background thread that walks the bootstrap, writes stage updates to
the mutex, then waits up to 60 s for the backend port to answer and
flips stage to `ready`.
- `ensure_venv_ready` + `spawn_backend` take the progress mutex
(Option<&Arc<Mutex<…>>>) and set the right stage at each step.
They now take AppHandle<R> instead of &App<R> so the background
thread can hold them.
React (frontend/src/components/BootstrapSplash.{jsx,css}):
- Self-contained splash component + `useBootstrapStage()` hook that
polls the Rust command every 1 s, short-circuits to 'ready' in the
Vite dev server / non-Tauri contexts.
- Renders a progress bar + step list keyed to BootstrapStage. On
Failed, shows the Rust-side message.
App.jsx:
- Calls `useBootstrapStage()`, blocks the main UI render until stage
=== 'ready'.
Also bumps pyproject/package/cargo/tauri.conf versions 0.2.2 → 0.2.3.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AppImage was dropped earlier when linuxdeploy's AppImage runtime
couldn't FUSE-mount on GH Actions runners. Now viable again because:
1. `APPIMAGE_EXTRACT_AND_RUN=1` bypasses FUSE (extract-and-run).
2. The thin uv-venv installer is ~10 MB (vs the prior ~2 GB PyInstaller
payload that tripped linuxdeploy's internal size limits).
Matrix `bundles` for Linux: `deb,updater` → `deb,appimage,updater`.
tauri.conf.json `targets` also updated so dev builds can produce
AppImages locally.
Covers universal Linux — runs on any glibc-2.31+ host without a
package manager.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First release cut with the uv-venv bootstrap (PR #16) + thin installer
architecture + CI cache layers. macOS Intel dropped from matrix;
ARM only.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple shipped the last Intel Mac in June 2023 and Rosetta 2 runs the
ARM build natively at 85-100% of native speed. macos-13 runner backlog
was also blocking every v0.2.0 retag for ~10 min waiting on a hosted
Intel runner — measurable pain for no measurable user reach.
If we ever need Intel builds back, the matrix entry is one block of
five YAML lines.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two unrelated tweaks grouped into one PR to keep churn low.
## Ports
Backend 8000 → 3900, Vite dev 5173 → 3901, 3902 reserved for future
IPC. Port 8000 conflicts with Django/Rails/Jupyter/Airflow on most
dev machines; the uncommon 3900 range dodges that. Touched:
- frontend/src-tauri/src/lib.rs (BACKEND_PORT)
- frontend/src-tauri/tauri.conf.json (devUrl)
- frontend/vite.config.js (server.port)
- frontend/src/api/client.ts (hardcoded API base)
- frontend/src/App.jsx (PREVIEW_API fallback)
- backend/main.py (CORS allowlist + uvicorn.run default)
Rust sidecar launcher and FastAPI uvicorn port stay in sync via the
`BACKEND_PORT` constant + explicit port=3900.
## CI caches
Build time shaves across ci.yml and release.yml:
- `astral-sh/setup-uv@v3` → `enable-cache: true` keyed on uv.lock
(~45 s saved per run after uv.lock stabilises)
- `awalsh128/cache-apt-pkgs-action` for ffmpeg (~25 s saved)
- `actions/cache@v4` on `~/.bun/install/cache` keyed on bun.lock
(~15 s saved; applied to both test gate and build matrix)
Expected warm test job: ~45-60 s (was ~2-3 min).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`encodeURIComponent("voxcpm2/voxcpm2-2B-EC")` turns the slash into
`%2F`, which some ASGI layers reject or fail to roundtrip through the
`:path` converter. Frontend was sending `/models/voxcpm2%2Fvoxcpm2-2B-EC`
and getting a silent no-op (or 404 swallowed by the busy-state wrapper).
Encode each path segment instead so special chars in the repo name
still escape but the slash survives as a literal `/` in the URL.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cargo dep compile is the long pole of each Tauri build now that
PyInstaller is out. Add Swatinem/rust-cache@v2 keyed by rust_target so
each matrix job (mac arm, mac intel, windows, linux) gets its own
cache. Caches ~/.cargo/registry + frontend/src-tauri/target.
Expected: cold first run stays ~5-7 min per platform; subsequent runs
on the same rust_target drop to ~1-2 min.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: cache Rust deps for Tauri build (~5 min → ~1-2 min on warm runs)
Cargo dep compile is the long pole of each Tauri build now that
PyInstaller is out. Add Swatinem/rust-cache@v2 keyed by rust_target so
each matrix job (mac arm, mac intel, windows, linux) gets its own
cache. Caches ~/.cargo/registry + frontend/src-tauri/target.
Expected: cold first run stays ~5-7 min per platform; subsequent runs
on the same rust_target drop to ~1-2 min.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(frontend): bump deps + TypeScript 6
- vite 8.0.8 → 8.0.9 (patch)
- eslint 10.2.0 → 10.2.1 (patch)
- eslint-plugin-react-hooks 7.0.1 → 7.1.1
- globals 17.4.0 → 17.5.0
- typescript 5.9.3 → 6.0.3 (major)
TS 6 warns on tsconfig's `baseUrl` ("deprecated, removed in 7.0"); add
`ignoreDeprecations: "6.0"` to keep the current path-alias setup until
we migrate off baseUrl in the next cycle.
Verified locally: `tsc --noEmit` clean, `vite build` produces bundles
identical in shape to the prior version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Supersedes the PyInstaller tarball approach from PR #15. PyInstaller
never made it past iteration — even with CPU-only torch + strip +
optimize, the Linux .deb and Windows MSI both overshot GH Releases'
2 GB per-asset cap. Trying to keep it under the cap also meant CUDA
was off the table for users who did have a GPU.
Switch to the bootstrap pattern Unsloth uses:
- Installer ships the Tauri shell + frontend dist + repo's
pyproject.toml + uv.lock + backend/ source tree as Tauri resources.
DMG is 8.9 MB (verified locally). MSI / .deb should be similar.
- On first launch, src-tauri/src/lib.rs::ensure_venv_ready() downloads
the standalone `uv` binary (if not already on PATH), copies the
bundled pyproject.toml + uv.lock + backend/ into
`app_local_data_dir/project`, then runs `uv venv --python 3.11`
+ `uv sync --frozen --no-dev`. Subsequent launches skip.
- spawn_backend launches `{venv_python} -m uvicorn main:app
--app-dir {project/backend}` — no more PyInstaller binary.
- Dev mode still wins: if `.venv` at the source tree exists, reuse it
(matches `bun run dev` behaviour).
Release workflow drops: Setup Python, Install uv, CPU-torch reinstall,
PyInstaller freeze, backend tarball package, and backend tarball upload
steps. CI now just builds Rust + bundles resources; the heavy deps
install happens once on each user's machine.
User impact:
- Tiny installer → instant download + install (no 300–700 MB tarball).
- First launch: ~5–10 min setup while uv materialises the venv. This
happens behind the initial webview splash; subsequent launches are
normal.
- Users get the right torch wheel for their box — CPU by default,
CUDA if they already have the drivers (uv resolves from pyproject).
- Updates: bumping deps = bump uv.lock + ship a new installer; no
PyInstaller rebuild needed.
Known follow-ups:
- Progress UI during first-run bootstrap (React splash polling a
Tauri command). Right now the webview stays on the loading screen.
- Retry / repair flow if bootstrap fails (network drop, etc.).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
All three desktop platforms previously built a single asset — installer +
PyInstaller backend bundled together — that overshot GH Releases' 2 GB
per-asset cap on Linux and Windows. The mac DMG got under the cap thanks
to HFS compression, but Linux .deb and Windows MSI couldn't. NSIS and WiX
both failed during their own size-bounded packaging steps too.
Split the two:
- Tauri installer ships WITHOUT the PyInstaller backend
(`tauri.conf.json` bundle.resources is now empty). Installer sizes drop
from ~1.8 GB to ~50 MB.
- CI packages the frozen backend as
`omnivoice-backend_<version>_<triple>.tar.gz` after tauri-action, and
uploads it to the same draft release via `gh release upload`. Each
tarball is gz-compressed + comfortably under 2 GB with the CPU-only
torch wheel + strip=True from earlier PRs.
- On first launch, `ensure_backend_ready()` checks three locations in
order: resource dir (legacy), app_local_data_dir (new home for the
downloaded backend), and the dev-mode `dist/` fallback. If none match,
it downloads the tarball matching the current platform + app version
from the GH Release and extracts into app_local_data_dir. Blocking on
first run, no-op thereafter.
- find_bundled_backend + backend_exe_name are platform-aware — they
append .exe on Windows and scan all three roots.
Dependencies added to src-tauri/Cargo.toml: ureq (HTTP), tar + flate2
(archive extract). No tokio — ureq is synchronous, which matches the
existing setup() flow.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The binary-filter hack from PR #13 broke Tauri's resource walker on
Linux:
resource path `.../dist/omnivoice-backend/_internal/libcufft.so.11`
doesn't exist
PyInstaller's accounting (hook-generated rerun manifests, resource
glob expansion) still referenced the files after they were filtered
out of `a.binaries`, so Tauri's build.rs saw a path that didn't
exist on disk. Removing the post-hoc filter drops that error.
Keep strip=True + optimize=2 — those alone should still shave hundreds
of MB from native libs + bytecode. If the CPU-only torch wheel (PR #11)
+ these two flags aren't enough to get under 2 GB on Linux/Windows,
the next step is splitting the backend into a separately-downloaded
payload rather than trying to force it into one installer asset.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: opt JavaScript actions into Node 24 runtime
GH deprecates Node 20 for JavaScript actions on 2026-09-16. The
deprecation warning surfaces on every run right now. Setting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at workflow level makes
actions/checkout, actions/setup-*, astral-sh/setup-uv, and
oven-sh/setup-bun all run on Node 24 without bumping action versions.
This is a runtime override only — our own test script still pins
Node 22 via actions/setup-node@v4 (required for
--experimental-strip-types).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(release): strip symbols + filter CUDA/CUDA-provider binaries, report size
Previous slim pass (PR #11, CPU-only torch + module excludes) still left
the frozen backend above GH Releases' 2 GB per-asset cap. Two more levers:
1. strip=True on EXE + COLLECT. Strips debug symbols from ELF/Mach-O
native libraries. libtorch_cpu.so and friends drop ~25-30%. No-op on
Windows (MSVC stores symbols in separate .pdb files).
2. optimize=2 in Analysis. Compiles embedded bytecode with -OO:
docstrings + assertions removed. ~50-80 MB off the PYZ archive.
3. Post-hoc binary filter after collect_all. Even with nvidia wheels
excluded as Python modules, collect_all('torch')/('onnxruntime') can
still pull the CUDA-runtime shared libs via their linker hints.
Pattern-match them out of a.binaries before PYZ.
4. Log bundle size after freeze so CI runs can be compared without
downloading artifacts.
If this round still overshoots 2 GB, the next step is splitting the
payload (thin installer + post-install download of the Python bundle).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GH deprecates Node 20 for JavaScript actions on 2026-09-16. The
deprecation warning surfaces on every run right now. Setting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at workflow level makes
actions/checkout, actions/setup-*, astral-sh/setup-uv, and
oven-sh/setup-bun all run on Node 24 without bumping action versions.
This is a runtime override only — our own test script still pins
Node 22 via actions/setup-node@v4 (required for
--experimental-strip-types).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linux .deb upload and Windows MSI build both hit GitHub Releases'
hard 2147483648-byte asset cap because the frozen backend was ~2.2 GB
on Linux/Windows. Root causes + fixes:
- PyPI's default torch/torchaudio wheels bundle the full CUDA runtime
(~1.8 GB of libcuda*, libcublas*, libcudnn*, libcufft*, libcusparse*,
etc.). We ship CPU-only inference from the desktop binary; GPU is
surfaced only when a user-installed driver is detected at runtime.
Re-install torch from download.pytorch.org/whl/cpu for the Linux and
Windows matrix jobs before PyInstaller freezes. macOS wheels don't
include CUDA so they skip this step.
- Expand backend.spec excludes: torch subpackages we never touch at
inference time (torch.distributed, torch._dynamo, torch._inductor,
torch._export, torch.testing, torch.onnx, torch.ao, torch.fx.
experimental, torch._functorch, torch.utils.tensorboard,
torch.utils.benchmark), torchaudio.prototype, and heavy pyproject
deps the backend never imports (gradio, tensorboardX, webdataset,
s3prl, funasr, pedalboard). Also drop test trees that collect_all
sweeps up (scipy.special.tests, numpy.f2py.tests, etc.).
Expected bundle size after trim: ~600-900 MB uncompressed on Linux /
Windows, well under the 2 GB cap for .deb and MSI.
Model weights were never bundled — they already download on first run
via the HF cache when the user hits the Dub / TTS / ASR flows. So no
user-visible behaviour changes; the app just ships without the libs
required for CUDA builds, which weren't callable on those runners
anyway.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prior run (24797827082) showed tauri ignored tauri.conf.json's
bundle.targets filter: Windows build still ran makensis (NSIS) despite
the config listing only msi. Explicitly pass `--bundles` per platform
via tauri-action args:
- macOS: app,dmg,updater
- Windows: msi,updater (avoids NSIS's 2 GB stub limit)
- Linux: deb,updater (drops unreliable AppImage/linuxdeploy step)
Also removed `appimage` from tauri.conf.json's targets list to match,
keeping config + CLI in sync.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linux (ubuntu-22.04 runner) was failing the linuxdeploy step because GH
runners disable FUSE. Setting APPIMAGE_EXTRACT_AND_RUN=1 tells AppImages
to extract-and-run instead of mounting via FUSE.
Windows (windows-2022) was failing makensis with "Internal compiler
error #12345: error mmapping file (1843463346, 33554432) is out of
range" — NSIS's 32-bit file handling can't build an installer whose
payload approaches the 2 GB boundary (the PyInstaller-frozen backend is
~1.7 GB). Switched the Windows bundle target from NSIS to MSI (WiX),
which uses cabinet archives that handle larger payloads.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
tauri-action invokes `npm run tauri build -- --target <triple>`. Without
the script, all matrix builds failed with:
npm error Missing script: "tauri"
The `@tauri-apps/cli` devDep exposes the `tauri` binary in node_modules/.bin,
so the script just forwards to it.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`bun run <script>` auto-aliases `node` to `bun` in script bodies, so
`bun run test` fails with "node: bad option: --experimental-strip-types"
because bun doesn't support that flag. Call node directly from the CI
step instead of going through the package.json script.
Also add setup-node to release.yml's test gate — it was missing entirely,
relying on bun's node shim.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI ubuntu runner shipped node with no TypeScript loader, so
`await import('.../client.ts')` in tests/frontend/apiClient.test.mjs
failed with ERR_UNKNOWN_FILE_EXTENSION. Locally on macOS bun was
handling the extension transparently.
Fix: pin Node 22 via actions/setup-node@v4 (which natively supports
--experimental-strip-types) and pass the flag in the frontend `test`
script. Type annotations in client.ts are stripped at import time,
test bodies stay untouched.
Verified locally with node v24 — 36/36 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
release.yml only fires on `push: tags: ['v*']` + workflow_dispatch, so the
test job it contained never ran on pull requests — PRs landed with no
automated test feedback.
Split into a dedicated ci.yml that runs backend pytest + frontend node:test
+ tsc on every pull_request + push to main. release.yml stays tag-only for
the heavy 4-platform Tauri matrix build.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## /setup/preflight endpoint (backend/api/routers/setup.py)
New one-shot health check the setup wizard calls before model install.
Probes every runtime requirement so GPU driver mismatches, missing
ffprobe, low RAM, stale AMD ROCm setups, and unreachable HF no longer
manifest as silent CPU fallbacks or opaque runtime errors.
Checks returned as {id, label, status, detail, fix?}:
- Operating system + arch
- Python runtime
- System RAM (fail <8 GB, warn <12 GB)
- Disk free on HF cache partition (fail <10 GB)
- HuggingFace cache writable
- FFmpeg (required)
- FFprobe (warn — some endpoints degrade without it)
- GPU acceleration — vendor-aware detection:
* Apple Silicon → MPS available?
* NVIDIA → nvidia-smi parse; fail if driver < R555
(cu128 wheels we ship need ≥ 555)
* AMD → rocm-smi detect; warn if torch not built w/ ROCm
* none/unknown → warn, CPU-only note
- Network reachability to huggingface.co:443
Aggregate: {ok, has_warnings, checks, device}. Wizard blocks forward-
nav on any fail; passes warnings through with a labelled Continue.
## SetupWizard 4-step flow (frontend/src/pages/SetupWizard.jsx)
Insert "System check" as step 1 between Welcome and Install models.
Renders preflight report with pass/warn/fail icons, inline fix
instructions, and a Re-check button for users who resolve a blocker
without restarting the app. Continue button labels shift based on
status ("All good — continue" / "Continue (with warnings)" /
"Resolve blockers to continue").
## Transcribe-stream error clarity
dub_core.py: move ASR / missing-audio preflight out of HTTP-status
error paths into in-stream `error` events, since EventSource on the
client can't read non-2xx bodies and previously surfaced 503s as
opaque "network error" strings. Users now see the actionable message
(e.g. "ASR isn't loaded yet — check Settings → Models") inline.
App.jsx: on transcribe-stream drop before any final segment, force-
close + reject with a pointed message instead of waiting for
EventSource auto-reconnect to thrash against a broken endpoint.
## API client (frontend/src/api/setup.ts)
Add PreflightReport / PreflightCheck / PreflightDevice types + preflight()
call matching the new endpoint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump Tauri app version to 0.2.0 in Cargo.toml, Cargo.lock, tauri.conf.json.
Update capabilities manifest and src/lib.rs entry to wire the new setup
wizard flow and sidecar management.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- ErrorBoundary wraps each lazy route (Launchpad/Clone-Design/Dub/Settings).
Crash in one tab → friendly fallback card; rest of app stays functional.
Errors surface to Settings > Logs > Frontend via console.error ring buffer.
- Sidebar search input (pill shape at top of scroll area) filters projects,
profiles, gen history, dub history, exports by name/text/seed/path.
- KeyboardCheatsheet: press '?' to open, grid of shortcut kbd pills
(Navigation / Segment editor / Audio trimmer / Dub). Mac+Windows key labels.
- Dub thumbnail: Launchpad DubProjects cards now load /dub/thumb/{id} via
DubThumb component with graceful fallback to film icon.
- Tauri cross-platform:
* tauri.conf.json keeps Overlay+hiddenTitle (macOS-only, ignored elsewhere)
so mac traffic lights float inside our Header instead of two-bar stack.
* tauri.macos.conf.json: transparent + minimumSystemVersion 12.0.
* tauri.windows.conf.json: NSIS + MSI bundles, webview bootstrapper.
* tauri.linux.conf.json: AppImage + deb + rpm, deb depends on ffmpeg
and libwebkit2gtk-4.1-0.
- Dub tab idle: right ghost panel hidden until upload; drop zone spans full
width. Converted 8 glass-panel wrappers to studio-panel.
- Sidebar dub history: filter out empty/und/Auto language tokens (no more '()').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Sidebar dub history subtitle: filter out empty/"und"/"Auto" language tokens
instead of rendering "English (und)" → "()" style literals.
- DubTab idle state: when no video loaded, hide right ghost panel and
let left drop-zone panel span both columns. Prevents looking "broken"
before upload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>