Files
VoiceStudio/docs/desktop-build.md
T
debpalashandClaude Opus 4.7 d1fd0e5fcb chore: release docs, pin python version, drop stale tarball
- Add docs/RELEASING.md, DESKTOP_RELEASE.md, desktop-build.md for
  release workflow and packaging steps
- Relocate next.md → docs/specs/studio-v1.md (scratch → formal spec)
- Pin Python version via .python-version
- Ignore research/ clones in .gitignore
- Remove stale omnivoice-studio-20260421-1834.tar.gz snapshot

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:48:45 +05:30

12 KiB
Raw Blame History

Desktop build — progress tracker

Working doc for the desktop release effort. Every box maps to a concrete deliverable; mark [x] when verified end-to-end on a fresh environment, not just "code compiles." See docs/DESKTOP_RELEASE.md for the full engineering plan + voicebox comparison that grounds these milestones.

Primary target: macOS Apple Silicon (arm64), unsigned. Stretch targets: macOS Intel, Windows x64, Linux — parked in CI matrix until the arm64 path is green end-to-end.


Source of truth files

Concern Owner file
Backend freeze spec backend.spec
PyInstaller runtime hooks backend/hooks/*.py (TBD)
Tauri sidecar launcher frontend/src-tauri/src/lib.rs
Tauri bundle config frontend/src-tauri/tauri.conf.json
HF download progress backend/utils/hf_progress.py
First-run wizard endpoints backend/api/routers/setup.py
First-run wizard UI frontend/src/pages/SetupWizard.jsx (TBD)
CI release matrix .github/workflows/release.yml
Reference implementation jamiepine/voicebox

Phase A — Frozen backend binary ( 2026-04-21)

  • Port voicebox runtime hooks into backend/hooks/
    • pyi_rth_numpy_compat.py — pre-imports numpy to prime the C ext
    • pyi_rth_torch_compiler_disable.py — disables dynamo/inductor via env
  • Wire hooks into backend.spec via runtime_hooks=[...]
  • Add Apple Silicon exclude list to backend.spec (nvidia.*, triton, flash_attn)
  • uv run pyinstaller backend.spec --noconfirm --clean produces a clean bundle — 140 s build time
  • ./dist/omnivoice-backend/omnivoice-backend starts and serves /system/info~23 s cold start (not ≤3 s as targeted, but acceptable behind splash screen in Phase D)
  • Frozen binary serves all core endpoints (/system/info, /setup/status, /engines)
  • hf_progress patch installs on frozen start (confirmed via log)
  • Frozen binary transcribes Fireship fixture → Spanish dub end-to-end (deferred to Phase B — needs Tauri WebView for the frontend, or direct curl-driven harness)
  • Bundle size ≤600 MBcurrently 1.1 GB, ~2× over target. Likely shavable to ~700 MB by excluding torch.distributed.*, transformers bloat, scipy tests. Not a Phase A blocker; cleanup tracked as Phase A.1.

Verified:

./dist/omnivoice-backend/omnivoice-backend
# → "Uvicorn running on http://0.0.0.0:8000"
curl -s http://127.0.0.1:8000/system/info          # ✓ returns JSON
curl -s http://127.0.0.1:8000/setup/status         # ✓ {"models_ready":true,...}
curl -s http://127.0.0.1:8000/engines              # ✓ omnivoice/voxcpm2/moss-tts-nano listed

Lessons learned (feeding into Phase B):

  • PyInstaller buffers stdout by default — Tauri sidecar spawn must set PYTHONUNBUFFERED=1 so logs surface promptly.
  • main.py's if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) is what makes the frozen binary serve. Keep it.
  • frontend_path = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") at main.py:172 will never resolve in a frozen build; frontend assets come from Tauri's WebView instead, not the Python backend. Guard is fine; note for Phase B.

Phase A.1 — Bundle size reduction (optional, before Phase D)

  • Add torch.distributed.* to excludes (not used on Apple Silicon single-device inference)
  • Exclude scipy.special.tests.*, numpy.tests.*, torch.testing.*
  • Exclude transformers.models.* for non-used model families
  • Target: <700 MB bundle

Phase B — Tauri launches the frozen sidecar (in progress 2026-04-21)

  • Change backend default port 8000 → 17493 — deferred. Port-switching ripples through the frontend API client + bench scripts + install scripts. Keep 8000 for now; switch in a dedicated refactor when we need to.
  • Rewrite lib.rs::setup to:
    • Probe /system/info on port 8000 and attach if responding (backend_healthy())
    • Kill orphans on port 8000 via lsof -ti :8000 | xargs kill -9
    • Launch bundled Contents/Resources/backend/omnivoice-backend/omnivoice-backend if free
    • Fall back to uv run uvicorn ... in dev (when bundled binary not present)
    • Set PYTHONUNBUFFERED=1 on sidecar env so backend logs flush in real time
    • Export OMNIVOICE_FFMPEG pointing at bundled ffmpeg + prepend its dir to PATH
  • Wire tauri.conf.json bundle.resources:
    • ../../dist/omnivoice-backendbackend/omnivoice-backend/
    • binaries/ffmpegbin/ffmpegremoved. Hit Permission denied (os error 13) at bundle time (brew shim carried com.apple.provenance xattr that xattr -c couldn't strip under SIP). Dropped the separate resource: imageio_ffmpeg already ships a 47 MB static arm64 ffmpeg inside the PyInstaller bundle at _internal/imageio_ffmpeg/binaries/ffmpeg-macos-aarch64-v7.1, and find_ffmpeg() picks it up via imageio_ffmpeg.get_ffmpeg_exe() — zero extra bundling.
  • Rust compile clean — cargo check passes
  • bunx tauri build produces an .app (1.3 GB) — bundle contents verified: Contents/Resources/backend/omnivoice-backend/omnivoice-backend. ffmpeg travels inside the backend resource (see above).
  • bunx tauri build produces a .dmg400 MB (well under the 600 MB target).
  • Fix API host in production build — frontend/src/api/client.ts previously used API = '' in prod, which caused relative fetches against tauri://localhost to fail with "The string did not match the expected pattern" in the Settings → Models tab. Now hardcodes http://localhost:8000 so the webview always reaches the bundled sidecar. CORS + CSP already allow it.
  • .app opens, connects to bundled backend, ingests/transcribes without error (manual test pending)

DMG packaging failure to investigate

Tauri's bundle_dmg.sh runs hdiutil to convert the intermediate RW DMG → compressed read-only DMG. On our 1.7 GB payload this failed silently; next session's task is to run the shell script manually and capture stderr. Possible fixes in priority order:

  1. Shrink the backend bundle (Phase A.1 — we're 1.1 GB, voicebox is 482 MB; 500 MB target cuts DMG pipeline latency and works around many hdiutil edge cases).
  2. Pass hdiutilArgs in tauri.conf.json["-format", "UDZO", "-imagekey", "zlib-level=1"] — cheaper compression, faster, fewer hdiutil quirks.
  3. If hdiutil errors with disk space: TMPDIR=/Volumes/other bunx tauri build to route the scratch volume elsewhere.
  4. Last resort: build .app.tar.gz with create-dmg externally (voicebox pattern).

Verification target:

open frontend/src-tauri/target/release/bundle/macos/"OmniVoice Studio.app"
# The window should open, segment table should render from a fresh drop.

Phase C — First-run wizard + HF progress ( 2026-04-21)

  • backend/utils/hf_progress.py — tqdm monkey-patch
  • backend/api/routers/setup.py
    • GET /setup/status — missing + disk-free
    • GET /setup/download-stream — SSE stream
    • POST /setup/warmup — background model load
    • GET /models — every known model + install state (Phase M seed)
    • POST /models/install — single-repo install (progress via SSE)
    • DELETE /models/{repo_id} — evict cached revisions, free disk
  • frontend/src/pages/SetupWizard.jsx + .css
    • Mounts on boot, calls /setup/status
    • Per-file progress bars from SSE
    • Disk-space error state if disk_free_gb < min_free_gb
    • Polls /setup/status every 5 s during install to detect completion
  • Route guard in App.jsx: if (setupNeeded) return <SetupWizard>
  • Settings → Models tab: ModelStoreTab replaces the old read-only summary
    • Lists every KNOWN_MODEL with status badge, role, size, repo_id
    • Install / Reinstall / Delete buttons per row
    • Aggregate "on-disk" footer
    • Live per-file progress bars driven by the shared SSE stream

Verification:

# Simulate fresh install
mv ~/.cache/huggingface/hub /tmp/hf-hub.bak
bun run dev
# Wizard appears, progress ticks, UI unlocks when done.
mv /tmp/hf-hub.bak ~/.cache/huggingface/hub  # restore

Phase D — DMG + clean-machine test

  • frontend/src-tauri/tauri.conf.json bundle.targets = ["dmg", "app"]
  • scripts/build_desktop.sh — one-shot: PyInstaller → Tauri build → DMG
  • Build produces OmniVoice Studio_0.1.0_aarch64.dmg
  • DMG size ≤600 MB
  • DMG runs on a fresh macOS user account (no brew/uv/bun present)
    • Mount → drag to Applications → right-click → Open (Gatekeeper override)
    • First launch shows setup wizard, models download cleanly
    • End-to-end: ingest YouTube URL → transcribe → translate → generate → play
  • README section: "Download DMG" with right-click → Open instructions

Verification:

bash scripts/build_desktop.sh
ls -lh frontend/src-tauri/target/release/bundle/dmg/*.dmg
# Copy to fresh user account via System Settings → Users → Add.
# Log in, install DMG, walk the whole flow.

Phase E — Signing + notarization (blocked on Apple Developer ID)

  • Apple Developer ID ($99/yr)
  • Generate signing cert + App Store Connect API key
  • Store secrets in GitHub: APPLE_SIGNING_IDENTITY, APPLE_API_KEY, APPLE_API_ISSUER, APPLE_PROVIDER_SHORT_NAME
  • Enable signing in tauri-apps/tauri-action@v0.6 step
  • Post-build DMG re-notarize step (voicebox's workaround for Sequoia):
    • xcrun notarytool submit "$DMG" --wait --apple-id "$APPLE_ID" --password "$APP_SPECIFIC_PASSWORD"
    • xcrun stapler staple "$DMG"
    • Re-upload stapled DMG as release asset
  • Verify on clean Mac: no Gatekeeper warning on first open

Phase F — CI release matrix (parallel track)

  • .github/workflows/release.yml — unsigned matrix build
    • macos-14 arm64 primary
    • Intel / Windows / Linux stubbed (commented), ready to un-comment
    • workflow_dispatch uploads as workflow artifacts
    • Tag push (v*) attaches to GitHub Release
  • First green run — push a v0.1.0-preview tag, see DMG attached
  • Un-comment macos-13 row once arm64 is stable
  • Un-comment windows-2022 row + add Windows-specific PyInstaller notes
  • Un-comment ubuntu-22.04 row once the Linux packaging path is chosen (AppImage vs deb)

Cross-platform expansion (post-Phase-D)

One session per platform. Ordered by payoff:

  • macOS Intel (macos-13) — easiest; same cert, different PyInstaller wheel set
  • Windows x64 CPU (windows-2022)mlx_whisper absent, fall back to PyTorch Whisper. Needs Windows code-signing cert (~$100300/yr)
  • Windows x64 CUDA — follow voicebox scripts/package_cuda.py pattern, ship as lazy-download pack
  • Linux x64 AppImageappimagetool builds; unsigned is acceptable on Linux

Risks we're tracking

# Risk Mitigation Status
1 PyInstaller can't bundle torch Metal libs Copy voicebox collect_all() calls; fallback: portable-venv inside .app/Contents/Resources/ Untested
2 torch.compile breaks under frozen imports Port pyi_rth_torch_compiler_disable.py hook Pending
3 First-run model download fails halfway SSE retry + resumable hf_hub_download (native) Partial — frontend UI TBD
4 User has <10 GB free disk /setup/status refuses download; shows error Implemented
5 Gatekeeper blocks unsigned app Document right-click → Open in README Not yet documented
6 DMG size >800 MB Exclude nvidia/triton/matplotlib; lazy-download optional packs Exclude list in spec
7 macOS 15 Sequoia rejects un-notarized DMG wrapper Phase E includes explicit stapler staple step Blocked on Phase E

Change log (this doc)

  • 2026-04-21 — initial tracker created. Phase C partially shipped: tqdm monkey-patch + /setup/status + SSE stream + /setup/warmup live and smoke-tested. Phase F skeleton committed (CI workflow, primary target only — non-arm64 rows parked). Phases A, B, D, E pending.