Compare commits
19
Commits
release/v0.2.0
...
v0.2.2
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+69
-1
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 5173,
|
||||
port: 3901,
|
||||
strictPort: true,
|
||||
host: false,
|
||||
watch: {
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user