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>
Updated the dev:api script to use `uv run` instead of a hardcoded virtual environment path.
Changes:
- replaced `.venv/bin/uvicorn` with `uv run uvicorn`
Reason:
The previous implementation relied on a POSIX-specific path, which breaks on Windows
(where executables are located in `.venv/Scripts`). Using `uv run` ensures the command
works consistently across different operating systems by resolving the environment automatically.
This update improves the README setup instructions to make them accurate and easier to follow.
Changes:
- Fixed incorrect repository clone URL
- Corrected project directory name in setup steps
- Clarified backend and frontend startup process
- Replaced OS-specific commands with cross-platform alternatives
Reason:
The previous instructions contained inconsistencies (e.g., wrong repository reference)
and OS-dependent commands that could lead to setup issues, especially on Windows.