Compare commits

...
19 Commits
Author SHA1 Message Date
Palash Debnath c7816b7222 chore: bump version to 0.2.2 (#24)
Adds Linux AppImage bundle (PR #23).
2026-04-23 07:55:16 +05:30
Palash DebnathandClaude Opus 4.7 8c27ba40dc feat(release): add Linux AppImage bundle (#23)
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>
2026-04-23 07:47:43 +05:30
Palash DebnathandClaude Opus 4.7 60e19e4eb3 chore: bump version to 0.2.1 (#22)
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>
2026-04-23 07:23:27 +05:30
Palash DebnathandClaude Opus 4.7 5a754b461e chore(release): drop macOS Intel from matrix (#21)
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>
2026-04-23 07:06:41 +05:30
Palash DebnathandClaude Opus 4.7 90ef02b2e4 chore: unique ports (3900/3901) + broader CI caches (#20)
Two unrelated tweaks grouped into one PR to keep churn low.

## Ports

Backend 8000 → 3900, Vite dev 5173 → 3901, 3902 reserved for future
IPC. Port 8000 conflicts with Django/Rails/Jupyter/Airflow on most
dev machines; the uncommon 3900 range dodges that. Touched:

- frontend/src-tauri/src/lib.rs (BACKEND_PORT)
- frontend/src-tauri/tauri.conf.json (devUrl)
- frontend/vite.config.js (server.port)
- frontend/src/api/client.ts (hardcoded API base)
- frontend/src/App.jsx (PREVIEW_API fallback)
- backend/main.py (CORS allowlist + uvicorn.run default)

Rust sidecar launcher and FastAPI uvicorn port stay in sync via the
`BACKEND_PORT` constant + explicit port=3900.

## CI caches

Build time shaves across ci.yml and release.yml:

- `astral-sh/setup-uv@v3` → `enable-cache: true` keyed on uv.lock
  (~45 s saved per run after uv.lock stabilises)
- `awalsh128/cache-apt-pkgs-action` for ffmpeg (~25 s saved)
- `actions/cache@v4` on `~/.bun/install/cache` keyed on bun.lock
  (~15 s saved; applied to both test gate and build matrix)

Expected warm test job: ~45-60 s (was ~2-3 min).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 06:52:30 +05:30
Palash DebnathandClaude Opus 4.7 e57448f704 fix(settings): delete model button — encode HF repo_id per segment (#19)
`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>
2026-04-23 06:38:38 +05:30
Palash DebnathandClaude Opus 4.7 715909d552 ci: cache Rust deps for Tauri build (~5 min → ~1-2 min on warm runs) (#17)
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>
2026-04-23 06:04:57 +05:30
Palash DebnathandClaude Opus 4.7 e354d27c56 chore(frontend): bump deps + TypeScript 6 (#18)
* 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>
2026-04-23 06:04:44 +05:30
Palash DebnathandClaude Opus 4.7 865897aeb7 feat(release): replace PyInstaller with uv-venv bootstrap, thin installers (#16)
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>
2026-04-23 05:54:39 +05:30
Palash DebnathandClaude Opus 4.7 c934ba2d7f feat(release): split backend out of installer, download + extract on first run (#15)
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>
2026-04-23 05:04:31 +05:30
Palash DebnathandClaude Opus 4.7 f2539b6719 fix(release): revert post-collect binary filter, keep strip+optimize only (#14)
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>
2026-04-23 03:42:39 +05:30
Palash DebnathandClaude Opus 4.7 d29db18214 fix(release): strip+filter bundle + report size (#13)
* 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>
2026-04-23 03:01:10 +05:30
Palash DebnathandClaude Opus 4.7 c3aa2e4533 ci: opt JavaScript actions into Node 24 runtime (#12)
GH deprecates Node 20 for JavaScript actions on 2026-09-16. The
deprecation warning surfaces on every run right now. Setting
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true at workflow level makes
actions/checkout, actions/setup-*, astral-sh/setup-uv, and
oven-sh/setup-bun all run on Node 24 without bumping action versions.

This is a runtime override only — our own test script still pins
Node 22 via actions/setup-node@v4 (required for
--experimental-strip-types).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 03:01:06 +05:30
Palash DebnathandClaude Opus 4.7 77fec801ba fix(release): slim PyInstaller bundle below GH's 2 GB per-asset limit (#11)
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>
2026-04-23 02:11:55 +05:30
Palash DebnathandClaude Opus 4.7 f79c9b1c70 fix(release): force per-platform bundle targets via --bundles CLI (#10)
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>
2026-04-23 01:20:18 +05:30
Palash DebnathandClaude Opus 4.7 092f3cff3f fix(release): Linux AppImage FUSE bypass + Windows NSIS→MSI (#9)
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>
2026-04-23 00:47:45 +05:30
Palash DebnathandClaude Opus 4.7 1256e7696a fix(release): add missing tauri npm script for tauri-action (#8)
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>
2026-04-23 00:11:33 +05:30
Palash DebnathandClaude Opus 4.7 dd16354a98 ci: invoke node directly for frontend tests; add setup-node to release workflow (#7)
`bun run <script>` auto-aliases `node` to `bun` in script bodies, so
`bun run test` fails with "node: bad option: --experimental-strip-types"
because bun doesn't support that flag. Call node directly from the CI
step instead of going through the package.json script.

Also add setup-node to release.yml's test gate — it was missing entirely,
relying on bun's node shim.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 23:49:59 +05:30
Palash Debnath fbbeaf3728 Merge pull request #6 from debpalash/release/v0.2.0
release: v0.2.0 — chrome theme, typography, preflight, export drawer, setup wizard
2026-04-22 23:23:44 +05:30
16 changed files with 559 additions and 203 deletions
+28 -4
View File
@@ -14,6 +14,10 @@ on:
permissions:
contents: read
env:
# Run all JavaScript actions on Node 24 (GH deprecates Node 20 in Sep 2026).
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
test:
name: Tests (backend + frontend)
@@ -26,8 +30,13 @@ jobs:
with:
python-version: "3.11"
# enable-cache persists ~/.cache/uv across runs, keyed on uv.lock —
# turns `uv sync` from ~45 s cold to ~5 s warm.
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Node 22 is needed for --experimental-strip-types so node:test can
# import .ts files directly from frontend/src/api/*.
@@ -39,10 +48,12 @@ jobs:
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# apt install ffmpeg is ~30 s every run; cache the resolved .debs.
- name: System deps (ffmpeg)
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: ffmpeg
version: 1.0
- name: Install Python deps
run: uv sync
@@ -50,6 +61,16 @@ jobs:
- name: Run pytest
run: uv run pytest tests/ -q --tb=short
# Cache ~/.bun/install/cache keyed on bun.lock — `bun install` drops
# from ~15 s cold to near-instant on warm cache.
- name: Cache bun deps
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install frontend deps
working-directory: frontend
run: bun install
@@ -58,6 +79,9 @@ jobs:
working-directory: frontend
run: bunx tsc --noEmit
# Invoke node directly (not `bun run test`) because `bun run` auto-aliases
# `node` to `bun` in script bodies, and bun doesn't support
# --experimental-strip-types.
- name: Run frontend node:test
working-directory: frontend
run: bun run test
run: node --experimental-strip-types --no-warnings --test ../tests/frontend/*.test.mjs
+76 -37
View File
@@ -31,6 +31,10 @@ on:
permissions:
contents: write # needed to attach artifacts + updater manifest to GH Release
env:
# Run all JavaScript actions on Node 24 (GH deprecates Node 20 in Sep 2026).
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
jobs:
# Fast gating job — runs backend pytest + frontend node:test + tsc on a
# single Linux runner. The matrix build below waits on this via `needs:`
@@ -46,19 +50,30 @@ jobs:
with:
python-version: "3.11"
# enable-cache persists ~/.cache/uv keyed on uv.lock.
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Node 22 is needed for --experimental-strip-types so node:test can
# import .ts files directly from frontend/src/api/*.
- name: Setup Node 22
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# Backend tests need ffmpeg (subprocess calls in fixtures) + the minimal
# apt deps pydub/imageio pull in. Model weights are mocked so no HF
# downloads happen.
# Backend tests need ffmpeg (subprocess calls in fixtures). Cache the
# resolved .debs so warm runs skip the apt-get update + install.
- name: System deps (ffmpeg)
run: |
sudo apt-get update
sudo apt-get install -y ffmpeg
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: ffmpeg
version: 1.0
- name: Install Python deps
run: uv sync
@@ -66,6 +81,14 @@ jobs:
- name: Run pytest
run: uv run pytest tests/ -q --tb=short
- name: Cache bun deps
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install frontend deps
working-directory: frontend
run: bun install
@@ -74,9 +97,12 @@ jobs:
working-directory: frontend
run: bunx tsc --noEmit
# Invoke node directly (not `bun run test`) because `bun run` auto-aliases
# `node` to `bun` in script bodies, and bun doesn't support
# --experimental-strip-types.
- name: Run frontend node:test
working-directory: frontend
run: bun run test
run: node --experimental-strip-types --no-warnings --test ../tests/frontend/*.test.mjs
build:
needs: test
@@ -88,21 +114,29 @@ jobs:
arch: aarch64-apple-darwin
label: "macOS Apple Silicon"
rust_target: aarch64-apple-darwin
bundles: "app,dmg,updater"
- os: macos-13
arch: x86_64-apple-darwin
label: "macOS Intel"
rust_target: x86_64-apple-darwin
# macOS Intel dropped: Apple shipped the last Intel Mac in 2023 and
# Rosetta 2 runs the ARM build natively. macos-13 runner backlog
# was also blocking every release tag for ~10 min.
# Windows: force MSI bundling via --bundles. NSIS fails at makensis
# because our PyInstaller payload approaches its ~2 GB stub limit.
- os: windows-2022
arch: x86_64-pc-windows-msvc
label: "Windows x64"
rust_target: x86_64-pc-windows-msvc
bundles: "msi,updater"
# Linux: ship .deb + .AppImage. AppImage is universal (no distro
# package-manager dep), runs on any glibc-2.31+ host. Now viable
# because the thin uv-venv installer is ~10 MB (vs the prior ~2 GB
# PyInstaller payload that exceeded linuxdeploy limits). FUSE
# unavailability on GH runners handled via APPIMAGE_EXTRACT_AND_RUN=1.
- os: ubuntu-22.04
arch: x86_64-unknown-linux-gnu
label: "Linux x64"
rust_target: x86_64-unknown-linux-gnu
bundles: "deb,appimage,updater"
runs-on: ${{ matrix.os }}
name: ${{ matrix.label }}
@@ -111,23 +145,27 @@ jobs:
- uses: actions/checkout@v4
# ── Language runtimes ──────────────────────────────────────────────
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install uv
uses: astral-sh/setup-uv@v3
- name: Setup Rust (stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.rust_target }}
# Cache ~/.cargo/registry + {target}/ per rust_target. Cargo dep
# compile is the long pole of the build — cold is ~5-7 min, warm
# drops to ~1-2 min.
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: frontend/src-tauri -> target
key: ${{ matrix.rust_target }}
- name: Setup Bun
uses: oven-sh/setup-bun@v1
# ── Platform deps ─────────────────────────────────────────────────
# ── Platform deps (Tauri host requirements only — no Python here)
# The runtime Python/uv bootstrap happens on the user's machine at
# first launch, not in CI. CI only packages the source (pyproject.toml,
# uv.lock, backend/*.py) into the Tauri installer as resources.
- name: macOS system deps
if: runner.os == 'macOS'
run: |
@@ -143,21 +181,15 @@ jobs:
libayatana-appindicator3-dev librsvg2-dev \
libasound2-dev ffmpeg
# Windows: ffmpeg ships via imageio_ffmpeg inside the PyInstaller bundle,
# no system-level install needed. PyInstaller picks it up automatically.
# ── Python deps + PyInstaller ──────────────────────────────────────
- name: Install Python deps
run: uv sync
- name: Freeze backend via PyInstaller
shell: bash
run: |
uv run pyinstaller backend.spec --noconfirm --clean
echo "Backend frozen at dist/omnivoice-backend/"
ls dist/omnivoice-backend/ | head
# ── Frontend build ─────────────────────────────────────────────────
- name: Cache bun deps
uses: actions/cache@v4
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock', 'bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install frontend deps
working-directory: frontend
run: bun install
@@ -165,16 +197,23 @@ jobs:
# ── Tauri build + sign + publish ───────────────────────────────────
# tauri-action handles: bundle, sign updater payload with the
# TAURI_SIGNING_PRIVATE_KEY secret, attach to release, update
# latest.json with per-platform download URLs & signatures.
# latest.json with per-platform download URLs & signatures. The
# installer ships the repo's pyproject.toml + uv.lock + backend/
# tree as Tauri resources; lib.rs::ensure_venv_ready recreates the
# venv on first launch via `uv sync --frozen --no-dev`.
- name: Build + release (Tauri)
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# GH runners disable FUSE, so linuxdeploy's AppImage can't mount
# itself at bundle time. This env tells linuxdeploy to extract-and-run
# instead, which works without FUSE.
APPIMAGE_EXTRACT_AND_RUN: 1
with:
projectPath: frontend
args: --target ${{ matrix.rust_target }}
args: --target ${{ matrix.rust_target }} --bundles ${{ matrix.bundles }}
tagName: ${{ github.ref_name }}
releaseName: "OmniVoice Studio ${{ github.ref_name }}"
releaseBody: "Auto-generated release. See commit log for changes."
+38 -6
View File
@@ -146,17 +146,46 @@ a = Analysis(
excludes=[
# Desktop-only bloat the frozen backend never uses.
'tkinter', 'matplotlib', 'PIL.ImageQt', 'PyQt5', 'PyQt6',
# CUDA / NVIDIA wheels on Apple Silicon — saves ~2 GB.
# When we add a Windows/Linux CUDA build, remove these per-target.
# CUDA / NVIDIA wheels on every platform — we ship CPU-only inference
# for the desktop app. Models download on first run via HF cache, and
# GPU use is surfaced only when a user-installed driver is detected
# at runtime. Excluding these saves ~2 GB per bundle, which is what
# keeps Linux .deb / Windows MSI under GH Releases' 2 GB asset cap.
'nvidia', 'nvidia.cublas', 'nvidia.cudnn', 'nvidia.cuda_runtime',
'nvidia.cuda_nvrtc', 'nvidia.nccl', 'nvidia.nvtx',
'nvidia.curand', 'nvidia.cusolver', 'nvidia.cusparse',
'nvidia.cufft', 'nvidia.cuda_cupti',
'nvidia.cufft', 'nvidia.cuda_cupti', 'nvidia.cusparselt',
'nvidia.nvjitlink', 'nvidia.cufile',
'triton', 'flash_attn',
# Torch internals we never invoke at inference time — distributed
# training, compile, FX tracing, tensorboard, testing helpers. These
# pull hundreds of MB of Python source + transitive deps.
'torch.distributed', 'torch._dynamo', 'torch._inductor',
'torch._export', 'torch.testing', 'torch.utils.tensorboard',
'torch.utils.benchmark', 'torch.fx.experimental',
'torch._functorch', 'torch.ao', 'torch.onnx',
# torchaudio prototype / deprecated — nothing in the backend touches
# these; removing saves tens of MB and silences the deprecation log
# noise on startup.
'torchaudio.prototype', 'torchaudio.models.hifigan',
# Heavy optional deps that are in pyproject.toml but the Studio
# backend never imports (verified with `grep ^import`). Excluding
# keeps them out of the frozen bundle; nothing on the runtime path
# breaks.
'gradio', 'gradio_client', 'tensorboardX', 'webdataset',
's3prl', 'funasr', 'pedalboard',
# Test / example trees that get swept up by collect_all.
'scipy.special.tests', 'scipy.tests', 'numpy.f2py.tests',
'numpy.tests', 'numpy.testing.tests',
],
noarchive=False,
optimize=0,
# optimize=2 compiles the embedded stdlib + site-packages with -OO,
# stripping assert statements + docstrings. Saves ~50-80 MB on a bundle
# this size. Runtime impact is negligible because we never inspect
# docstrings at runtime.
optimize=2,
)
pyz = PYZ(a.pure)
exe = EXE(
@@ -167,7 +196,10 @@ exe = EXE(
name='omnivoice-backend',
debug=False,
bootloader_ignore_signals=False,
strip=False,
# strip=True removes debug symbols from ELF/Mach-O binaries (no-op on
# Windows since MSVC doesn't emit symbols in the same way). Saves
# 10-30% on native libraries like libtorch_cpu.so (~300 MB → ~220 MB).
strip=True,
upx=False, # UPX often corrupts ML native libs — disabled.
console=True,
disable_windowed_traceback=False,
@@ -180,7 +212,7 @@ coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
strip=True,
upx=False,
upx_exclude=[],
name='omnivoice-backend',
+5 -3
View File
@@ -157,7 +157,7 @@ async def global_exception_handler(request: Request, exc: Exception):
_allowed = os.environ.get(
"OMNIVOICE_ALLOWED_ORIGINS",
"http://localhost:5173,http://127.0.0.1:5173,tauri://localhost,http://tauri.localhost",
"http://localhost:3901,http://127.0.0.1:3901,tauri://localhost,http://tauri.localhost",
).split(",")
app.add_middleware(
@@ -191,8 +191,10 @@ if os.path.exists(frontend_path):
else:
@app.get("/")
def _dev_fallback():
return RedirectResponse(url="http://localhost:5173")
return RedirectResponse(url="http://localhost:3901")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
# Port 3900 picked to dodge common 8000 conflicts (Django/Rails/Jupyter).
# Rust sidecar launcher in lib.rs::BACKEND_PORT must stay in sync.
uvicorn.run(app, host="0.0.0.0", port=3900)
+33 -35
View File
@@ -13,14 +13,14 @@
},
"frontend": {
"name": "omnivoice-studio",
"version": "0.0.0",
"version": "0.2.0",
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/source-serif-4": "^5.2.9",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@tauri-apps/plugin-dialog": "^2.7.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"@tauri-apps/plugin-window-state": "^2.4.1",
"lucide-react": "^1.8.0",
"react": "^19.2.5",
@@ -28,7 +28,7 @@
"react-hot-toast": "^2.6.0",
"react-window": "^2.2.7",
"wavesurfer.js": "^7.12.6",
"zustand": "^5.0.2",
"zustand": "^5.0.12",
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -37,12 +37,12 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "^5.7.3",
"vite": "^8.0.8",
"globals": "^17.5.0",
"typescript": "^6.0.3",
"vite": "^8.0.9",
},
},
},
@@ -137,39 +137,39 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@oxc-project/types": ["@oxc-project/types@0.124.0", "", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="],
"@oxc-project/types": ["@oxc-project/types@0.126.0", "", {}, "sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.16", "", { "os": "android", "cpu": "arm64" }, "sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.15", "", { "os": "darwin", "cpu": "x64" }, "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.15", "", { "os": "freebsd", "cpu": "x64" }, "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm" }, "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm" }, "sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "ppc64" }, "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "s390x" }, "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "s390x" }, "sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.16", "", { "os": "linux", "cpu": "x64" }, "sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.15", "", { "os": "linux", "cpu": "x64" }, "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.16", "", { "os": "linux", "cpu": "x64" }, "sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.15", "", { "os": "none", "cpu": "arm64" }, "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.16", "", { "os": "none", "cpu": "arm64" }, "sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.15", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.3" }, "cpu": "none" }, "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.16", "", { "dependencies": { "@emnapi/core": "1.9.2", "@emnapi/runtime": "1.9.2", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "arm64" }, "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.15", "", { "os": "win32", "cpu": "x64" }, "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.16", "", { "os": "win32", "cpu": "x64" }, "sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
@@ -305,9 +305,9 @@
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
"eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="],
"eslint": ["eslint@10.2.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.5.5", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g=="],
"eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
@@ -359,7 +359,7 @@
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
"globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
"globals": ["globals@17.5.0", "", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="],
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
@@ -473,7 +473,7 @@
"picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
"postcss": ["postcss@8.5.9", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="],
"postcss": ["postcss@8.5.10", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ=="],
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
@@ -491,7 +491,7 @@
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"rolldown": ["rolldown@1.0.0-rc.15", "", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
"rolldown": ["rolldown@1.0.0-rc.16", "", { "dependencies": { "@oxc-project/types": "=0.126.0", "@rolldown/pluginutils": "1.0.0-rc.16" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.16", "@rolldown/binding-darwin-arm64": "1.0.0-rc.16", "@rolldown/binding-darwin-x64": "1.0.0-rc.16", "@rolldown/binding-freebsd-x64": "1.0.0-rc.16", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.16", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.16", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.16", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.16", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.16", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.16", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.16", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.16" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g=="],
"rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
@@ -529,7 +529,7 @@
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@8.0.8", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="],
"vite": ["vite@8.0.9", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.10", "rolldown": "1.0.0-rc.16", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw=="],
"wait-on": ["wait-on@9.0.5", "", { "dependencies": { "axios": "^1.15.0", "joi": "^18.1.2", "lodash": "^4.18.1", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, "bin": { "wait-on": "bin/wait-on" } }, "sha512-qgnbHDfDTRIp73ANEJNRW/7kn8CrDUcvZz18xotJQku/P4saTGkbIzvnMZebPmVvVNUiRq1qWAPyqCH+W4H8KA=="],
@@ -561,8 +561,6 @@
"chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"omnivoice-studio/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.16", "", {}, "sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA=="],
}
}
+11 -10
View File
@@ -1,7 +1,7 @@
{
"name": "omnivoice-studio",
"private": true,
"version": "0.2.0",
"version": "0.2.2",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,15 +10,16 @@
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"test": "node --experimental-strip-types --no-warnings --test ../tests/frontend/*.test.mjs",
"preview": "vite preview"
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@fontsource-variable/source-serif-4": "^5.2.9",
"@fontsource/ibm-plex-mono": "^5.2.7",
"@tauri-apps/plugin-dialog": "^2.7.0",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-updater": "^2.10.1",
"@tauri-apps/plugin-window-state": "^2.4.1",
"lucide-react": "^1.8.0",
"react": "^19.2.5",
@@ -26,7 +27,7 @@
"react-hot-toast": "^2.6.0",
"react-window": "^2.2.7",
"wavesurfer.js": "^7.12.6",
"zustand": "^5.0.2"
"zustand": "^5.0.12"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -35,11 +36,11 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.2.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint": "^10.2.1",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.4.0",
"typescript": "^5.7.3",
"vite": "^8.0.8"
"globals": "^17.5.0",
"typescript": "^6.0.3",
"vite": "^8.0.9"
}
}
+69 -1
View File
@@ -79,10 +79,12 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
name = "app"
version = "0.2.0"
dependencies = [
"flate2",
"libc",
"log",
"serde",
"serde_json",
"tar",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
@@ -90,6 +92,8 @@ dependencies = [
"tauri-plugin-process",
"tauri-plugin-updater",
"tauri-plugin-window-state",
"ureq",
"zip 2.4.2",
]
[[package]]
@@ -3131,6 +3135,7 @@ version = "0.23.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
@@ -4074,7 +4079,7 @@ dependencies = [
"tokio",
"url",
"windows-sys 0.60.2",
"zip",
"zip 4.6.1",
]
[[package]]
@@ -4635,6 +4640,22 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64 0.22.1",
"flate2",
"log",
"once_cell",
"rustls",
"rustls-pki-types",
"url",
"webpki-roots 0.26.11",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -4954,6 +4975,24 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.7",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webview2-com"
version = "0.38.2"
@@ -5739,6 +5778,23 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "zip"
version = "2.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.14.0",
"memchr",
"thiserror 2.0.18",
"zopfli",
]
[[package]]
name = "zip"
version = "4.6.1"
@@ -5756,3 +5812,15 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]
+15 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "app"
version = "0.2.0"
version = "0.2.2"
description = "A Tauri App"
authors = ["you"]
license = ""
@@ -28,5 +28,19 @@ tauri-plugin-window-state = "2.0.0"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
# First-run bootstrap: the installer ships ~10 MB with only the Tauri
# shell + pyproject.toml + uv.lock + backend source. On first launch the
# Rust setup hook downloads the standalone `uv` binary, creates a venv at
# `app_local_data_dir/venv`, and runs `uv sync` against the bundled
# pyproject.toml — which installs torch, whisperx, faster-whisper, etc.
# ureq fetches the `uv` archive; flate2 + tar extract it on Unix; zip
# does the same on Windows (Astral publishes .zip for Windows only).
ureq = "2"
tar = "0.4"
flate2 = "1"
[target.'cfg(windows)'.dependencies]
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
+261 -92
View File
@@ -1,12 +1,22 @@
use std::fs;
use std::io::{self, Read};
use std::net::TcpStream;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use std::time::Duration;
use tauri::Manager;
const BACKEND_PORT: u16 = 8000;
// Unique port range (3900-3902) chosen to avoid common conflicts:
// 8000 collides with Django/Rails/Jupyter/Airflow on most dev machines.
// 3900 is the backend (FastAPI + uvicorn), 3901 is the Vite dev server,
// 3902 is reserved for future IPC / websocket listeners.
const BACKEND_PORT: u16 = 3900;
// Version of the Astral `uv` binary we download at first run when no system
// uv is on PATH. Pinned for reproducibility — bump alongside the uv.lock
// when the toolchain needs a newer uv.
const UV_VERSION: &str = "0.11.7";
pub struct BackendState {
pub process: Mutex<Option<Child>>,
@@ -103,23 +113,224 @@ fn kill_orphan_on_port(port: u16) {
#[cfg(not(unix))]
fn kill_orphan_on_port(_port: u16) {}
// ── Backend path resolution ───────────────────────────────────────────────
// ── First-run venv bootstrap (uv + `uv sync` against bundled pyproject) ──
/// Look for a frozen PyInstaller bundle shipped as a Tauri resource.
/// In a packaged .app: `Contents/Resources/backend/omnivoice-backend/omnivoice-backend`.
fn find_bundled_backend<R: tauri::Runtime>(app: &tauri::App<R>) -> Option<PathBuf> {
let resource_dir = app.path().resource_dir().ok()?;
let candidates = [
resource_dir.join("backend/omnivoice-backend/omnivoice-backend"),
resource_dir.join("backend/omnivoice-backend"),
resource_dir.join("omnivoice-backend"),
];
for c in &candidates {
if c.is_file() {
return Some(c.clone());
/// URL for the standalone `uv` release matching the host platform, plus a
/// flag indicating whether the archive is a .zip (Windows) vs .tar.gz (Unix).
fn uv_download_url() -> Option<(String, bool)> {
let triple = match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") => "aarch64-apple-darwin",
("macos", "x86_64") => "x86_64-apple-darwin",
("linux", "x86_64") => "x86_64-unknown-linux-gnu",
("windows", "x86_64") => "x86_64-pc-windows-msvc",
_ => return None,
};
let is_zip = cfg!(windows);
let ext = if is_zip { "zip" } else { "tar.gz" };
Some((
format!(
"https://github.com/astral-sh/uv/releases/download/{}/uv-{}.{}",
UV_VERSION, triple, ext
),
is_zip,
))
}
/// Download and extract the standalone `uv` binary into `dest`. Idempotent:
/// if the binary is already present, returns its path immediately.
fn install_uv_standalone(dest: &Path) -> io::Result<PathBuf> {
let uv_bin = dest.join(if cfg!(windows) { "uv.exe" } else { "uv" });
if uv_bin.is_file() {
return Ok(uv_bin);
}
let (url, is_zip) = uv_download_url().ok_or_else(|| {
io::Error::new(io::ErrorKind::Unsupported, "no uv binary for this platform")
})?;
log::info!("Downloading uv: {}", url);
fs::create_dir_all(dest)?;
let resp = ureq::get(&url)
.timeout(Duration::from_secs(120))
.call()
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("uv download: {}", e)))?;
if resp.status() != 200 {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("uv download HTTP {} from {}", resp.status(), url),
));
}
let mut reader = resp.into_reader();
if is_zip {
#[cfg(windows)]
{
let mut buf = Vec::new();
reader.read_to_end(&mut buf)?;
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(buf))
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("zip: {}", e)))?;
archive
.extract(dest)
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("zip extract: {}", e)))?;
}
#[cfg(not(windows))]
{
// Read the stream to avoid an unused-var warning on non-windows.
let mut _buf = Vec::new();
reader.read_to_end(&mut _buf)?;
return Err(io::Error::new(
io::ErrorKind::Unsupported,
"zip branch compiled on non-windows platform",
));
}
} else {
let gz = flate2::read::GzDecoder::new(reader);
let mut archive = tar::Archive::new(gz);
archive.unpack(dest)?;
}
// Astral's tarball extracts to `uv-<triple>/uv` on Unix. Find it and
// move to the stable `dest/uv` path. On Windows the .zip extracts to
// the root, so `uv.exe` is already at `dest/uv.exe`.
if uv_bin.is_file() {
return Ok(uv_bin);
}
for entry in fs::read_dir(dest)? {
let entry = entry?;
let p = entry.path();
if p.is_dir() {
let candidate = p.join(if cfg!(windows) { "uv.exe" } else { "uv" });
if candidate.is_file() {
let _ = fs::rename(&candidate, &uv_bin);
if uv_bin.is_file() {
return Ok(uv_bin);
}
fs::copy(&candidate, &uv_bin)?;
return Ok(uv_bin);
}
}
}
None
Err(io::Error::new(io::ErrorKind::NotFound, "uv binary not found after extract"))
}
fn venv_python_path(venv: &Path) -> PathBuf {
if cfg!(windows) {
venv.join("Scripts").join("python.exe")
} else {
venv.join("bin").join("python")
}
}
/// Recursive directory copy that skips `__pycache__` and any dotfile dirs.
fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let file_name = entry.file_name();
let name_str = file_name.to_string_lossy();
if src_path.is_dir() {
if name_str == "__pycache__" || name_str.starts_with('.') {
continue;
}
copy_dir_recursive(&src_path, &dst.join(&file_name))?;
} else if name_str.ends_with(".pyc") {
continue;
} else {
fs::copy(&src_path, &dst.join(&file_name))?;
}
}
Ok(())
}
/// Prepare (and on first run, create) the Python venv that will host the
/// backend process. Returns (venv_python, backend_source_dir).
///
/// Dev mode wins: if `.venv` exists at the project root, reuse it (matches
/// the behaviour of `bun run dev`). Otherwise copy the bundled pyproject.toml
/// + uv.lock + backend/ from Tauri resources into `app_local_data_dir/project`
/// and run `uv venv` + `uv sync --frozen --no-dev` there.
fn ensure_venv_ready<R: tauri::Runtime>(app: &tauri::App<R>) -> Option<(PathBuf, PathBuf)> {
if let Some(dev_root) = find_dev_project_root() {
let dev_venv = dev_root.join(".venv");
let dev_py = venv_python_path(&dev_venv);
if dev_py.is_file() {
let backend_dir = dev_root.join("backend");
if backend_dir.is_dir() {
return Some((dev_py, backend_dir));
}
}
}
let app_data = app.path().app_local_data_dir().ok()?;
let project_dir = app_data.join("project");
let venv_dir = project_dir.join(".venv");
let venv_py = venv_python_path(&venv_dir);
let backend_dir = project_dir.join("backend");
if venv_py.is_file() && backend_dir.is_dir() {
return Some((venv_py, backend_dir));
}
let resource_dir = app.path().resource_dir().ok()?;
let resource_pyproject = resource_dir.join("pyproject.toml");
let resource_uvlock = resource_dir.join("uv.lock");
let resource_backend = resource_dir.join("backend");
if !resource_pyproject.is_file() || !resource_backend.is_dir() {
log::warn!(
"Missing bootstrap resources (pyproject={}, backend={})",
resource_pyproject.display(),
resource_backend.display()
);
return None;
}
log::info!("First-run venv bootstrap in {}", project_dir.display());
if let Err(e) = fs::create_dir_all(&project_dir) {
log::error!("mkdir {} failed: {}", project_dir.display(), e);
return None;
}
if let Err(e) = fs::copy(&resource_pyproject, project_dir.join("pyproject.toml")) {
log::error!("copy pyproject.toml: {}", e);
return None;
}
if resource_uvlock.is_file() {
let _ = fs::copy(&resource_uvlock, project_dir.join("uv.lock"));
}
if let Err(e) = copy_dir_recursive(&resource_backend, &backend_dir) {
log::error!("copy backend/: {}", e);
return None;
}
// Prefer a system `uv` if one is on PATH; otherwise download the
// standalone binary into `app_data/tools`.
let uv_path = match Command::new("uv").arg("--version").output() {
Ok(_) => PathBuf::from("uv"),
Err(_) => match install_uv_standalone(&app_data.join("tools")) {
Ok(p) => p,
Err(e) => {
log::error!("uv install failed: {}", e);
return None;
}
},
};
log::info!("Bootstrap uv: {}", uv_path.display());
let status = Command::new(&uv_path)
.args(["venv", "--python", "3.11"])
.current_dir(&project_dir)
.status();
if !matches!(status, Ok(s) if s.success()) {
log::error!("uv venv failed: {:?}", status);
return None;
}
let sync_status = Command::new(&uv_path)
.args(["sync", "--frozen", "--no-dev"])
.current_dir(&project_dir)
.status();
if !matches!(sync_status, Ok(s) if s.success()) {
log::error!("uv sync failed: {:?}", sync_status);
return None;
}
Some((venv_py, backend_dir))
}
/// Dev-mode fallback: running from the source tree (`bun run dev`).
@@ -138,24 +349,6 @@ fn find_dev_project_root() -> Option<PathBuf> {
None
}
fn find_uv() -> String {
if Command::new("uv").arg("--version").output().is_ok() {
return "uv".to_string();
}
let home = std::env::var("HOME").unwrap_or_default();
for cand in [
format!("{}/.local/bin/uv", home),
format!("{}/.cargo/bin/uv", home),
"/opt/homebrew/bin/uv".to_string(),
"/usr/local/bin/uv".to_string(),
] {
if PathBuf::from(&cand).exists() {
return cand;
}
}
"uv".to_string()
}
fn backend_log_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
let log_dir = PathBuf::from(&home).join("Library/Logs/OmniVoice");
@@ -184,7 +377,7 @@ fn find_bundled_ffmpeg<R: tauri::Runtime>(app: &tauri::App<R>) -> Option<PathBuf
None
}
// ── Spawn the backend (bundled first, uv-fallback in dev) ─────────────────
// ── Spawn the backend via the bootstrapped venv Python ────────────────────
fn spawn_backend<R: tauri::Runtime>(app: &tauri::App<R>) -> Option<Child> {
let log_path = backend_log_path();
@@ -195,81 +388,62 @@ fn spawn_backend<R: tauri::Runtime>(app: &tauri::App<R>) -> Option<Child> {
err_path.display(),
);
let (python, backend_dir) = match ensure_venv_ready(app) {
Some(x) => x,
None => {
log::error!("Venv bootstrap failed — backend not started");
return None;
}
};
let stdout_file = fs::File::create(&log_path).ok();
let stderr_file = fs::File::create(&err_path).ok();
// Common env: unbuffered Python stdout, bundled ffmpeg path if we have one.
let mut env: Vec<(String, String)> = vec![
("PYTHONUNBUFFERED".into(), "1".into()),
];
let mut env: Vec<(String, String)> = vec![("PYTHONUNBUFFERED".into(), "1".into())];
if let Some(ff) = find_bundled_ffmpeg(app) {
env.push(("OMNIVOICE_FFMPEG".into(), ff.to_string_lossy().into_owned()));
let path_sep = if cfg!(windows) { ";" } else { ":" };
env.push((
"PATH".into(),
format!(
"{}:{}",
"{}{}{}",
ff.parent().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default(),
path_sep,
std::env::var("PATH").unwrap_or_default(),
),
));
}
// ── Production path — bundled PyInstaller binary ──
if let Some(bin) = find_bundled_backend(app) {
log::info!("Using bundled backend: {}", bin.display());
let mut cmd = Command::new(&bin);
for (k, v) in &env {
cmd.env(k, v);
}
let child = cmd
.stdout(stdout_file.map(Stdio::from).unwrap_or_else(Stdio::null))
.stderr(stderr_file.map(Stdio::from).unwrap_or_else(Stdio::null))
.spawn();
return match child {
Ok(c) => {
log::info!("Bundled backend started (pid {})", c.id());
Some(c)
}
Err(e) => {
log::error!("Failed to spawn bundled backend: {}", e);
None
}
};
}
// ── Dev fallback — uv run uvicorn over the source tree ──
log::info!("Bundled backend not found — falling back to `uv run uvicorn`");
let root = match find_dev_project_root() {
Some(r) => r,
None => {
log::warn!("Could not find project root; backend not started.");
return None;
}
};
let uv = find_uv();
let mut cmd = Command::new(&uv);
let mut cmd = Command::new(&python);
for (k, v) in &env {
cmd.env(k, v);
}
let child = cmd
.args([
"run", "uvicorn",
"-m",
"uvicorn",
"main:app",
"--app-dir", "backend",
"--host", "127.0.0.1",
"--port", &BACKEND_PORT.to_string(),
"--app-dir",
backend_dir.to_string_lossy().as_ref(),
"--host",
"127.0.0.1",
"--port",
&BACKEND_PORT.to_string(),
])
.current_dir(&root)
.stdout(stdout_file.map(Stdio::from).unwrap_or_else(Stdio::null))
.stderr(stderr_file.map(Stdio::from).unwrap_or_else(Stdio::null))
.spawn();
match child {
Ok(c) => {
log::info!("Dev backend started via uv (pid {})", c.id());
log::info!(
"Backend started via venv python {} (pid {})",
python.display(),
c.id()
);
Some(c)
}
Err(e) => {
log::error!("Failed to spawn dev backend: {}. Is `uv` installed?", e);
log::error!("Failed to spawn backend: {}", e);
None
}
}
@@ -300,23 +474,18 @@ pub fn run() {
// ── Port-reuse dance ──
// 1. TAURI_SKIP_BACKEND=1 → never spawn (for devs running uvicorn manually).
// 2. Packaged build (bundled binary present) → always own the sidecar:
// kill whatever's on 8000 and spawn our child. Attaching to an
// external backend looks fine at launch but leaves us stranded
// when that process dies — user sees silent "Load failed" errors
// and we can't recover.
// 3. Dev build (no bundled binary) + port already healthy → attach
// so you can keep a manual `uv run uvicorn` running alongside
// 2. Port already serving a healthy OmniVoice backend → attach so
// you can keep a manual `uv run uvicorn` running alongside
// `bun run tauri dev`.
// 4. Otherwise → spawn (kill orphan first if port held by corpse).
// 3. Otherwise → spawn (kill orphan first if port held by corpse).
// spawn_backend triggers the first-run venv bootstrap if needed.
let skip_spawn = std::env::var("TAURI_SKIP_BACKEND").is_ok();
let has_bundled = find_bundled_backend(app).is_some();
let child = if skip_spawn {
log::info!("TAURI_SKIP_BACKEND set — not spawning");
None
} else if !has_bundled && backend_healthy(BACKEND_PORT) {
} else if backend_healthy(BACKEND_PORT) {
log::info!(
"Dev mode: port {} already serving OmniVoice backend — attaching",
"Port {} already serving OmniVoice backend — attaching",
BACKEND_PORT
);
None
+8 -6
View File
@@ -1,11 +1,11 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "OmniVoice Studio",
"version": "0.2.0",
"version": "0.2.2",
"identifier": "com.debpalash.omnivoice-studio",
"build": {
"frontendDist": "../dist",
"devUrl": "http://localhost:5173",
"devUrl": "http://localhost:3901",
"beforeDevCommand": "bun run dev",
"beforeBuildCommand": "bun run build"
},
@@ -35,7 +35,7 @@
},
"bundle": {
"active": true,
"targets": ["dmg", "app", "nsis", "appimage", "deb"],
"targets": ["dmg", "app", "msi", "deb", "appimage"],
"createUpdaterArtifacts": true,
"icon": [
"icons/32x32.png",
@@ -44,9 +44,11 @@
"icons/icon.icns",
"icons/icon.ico"
],
"resources": {
"../../dist/omnivoice-backend": "backend/omnivoice-backend"
},
"resources": [
"../../pyproject.toml",
"../../uv.lock",
"../../backend"
],
"macOS": {
"minimumSystemVersion": "12.0"
}
+1 -1
View File
@@ -68,7 +68,7 @@ const doubleClickMaximize = () => {
* We upload to the backend's /preview endpoint and serve via HTTP instead.
* Falls back to createObjectURL for regular browsers.
*/
const _PREVIEW_API = import.meta.env.VITE_OMNIVOICE_API || 'http://localhost:8000';
const _PREVIEW_API = import.meta.env.VITE_OMNIVOICE_API || 'http://localhost:3900';
const fileToMediaUrl = async (file, prevUrls) => {
// Revoke previous blob URLs if they exist
if (prevUrls?.videoUrl?.startsWith('blob:')) URL.revokeObjectURL(prevUrls.videoUrl);
+4 -3
View File
@@ -1,8 +1,9 @@
// Backend always listens on localhost:8000 — both in dev (Vite @ 5173 talking
// Backend always listens on localhost:3900 — both in dev (Vite @ 3901 talking
// to a separate uvicorn) and in the built .app (Tauri webview @ tauri://localhost
// talking to the bundled frozen backend sidecar). Relative fetches against
// talking to the venv-bootstrapped sidecar). Relative fetches against
// tauri://localhost don't reach the sidecar, so we hardcode the absolute host.
export const API = 'http://localhost:8000';
// Port 3900 chosen to avoid common 8000 conflicts (Django/Rails/Jupyter).
export const API = 'http://localhost:3900';
export class ApiError extends Error {
status?: number;
+6 -1
View File
@@ -97,7 +97,12 @@ export async function getRecommendations(): Promise<Recommendations> {
}
export async function deleteModel(repo_id: string): Promise<{ deleted: boolean; repo_id: string; freed_bytes: number }> {
const r = await apiFetch(`/models/${encodeURIComponent(repo_id)}`, { method: 'DELETE' });
// HF repo_ids look like "owner/name" — encode each segment so special chars
// are escaped but the literal "/" survives into FastAPI's `:path` converter.
// encodeURIComponent on the whole string would turn "/" into "%2F", which
// some ASGI middleware rejects as a path-traversal attempt.
const path = repo_id.split('/').map(encodeURIComponent).join('/');
const r = await apiFetch(`/models/${path}`, { method: 'DELETE' });
return r.json();
}
+2 -1
View File
@@ -19,7 +19,8 @@
"noImplicitAny": false,
"noEmit": true,
"baseUrl": "./src",
"types": ["vite/client"]
"types": ["vite/client"],
"ignoreDeprecations": "6.0"
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js", "src/**/*.jsx"],
"exclude": ["node_modules", "dist", "src-tauri"]
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
},
},
server: {
port: 5173,
port: 3901,
strictPort: true,
host: false,
watch: {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "omnivoice"
version = "0.2.0"
version = "0.2.2"
description = "OmniVoice: Towards Omnilingual Zero-Shot Text-to-Speech with Diffusion Language Models"
readme = "README.md"
license = "Apache-2.0"