Fix fresh-clone desktop development by creating the required dist placeholder before Tauri starts, and make source setup install the selected CUDA or ROCm PyTorch stack consistently. Adds behavior-level cross-platform regression coverage.\n\nFixes #1664.\nFixes #1665.\n\nThanks @uberclokr for the contribution.
This commit is contained in:
@@ -44,6 +44,8 @@ the frozen-backend fallback mirror it for their toolchains.
|
||||
- The OmniVoice guide now covers combining style attributes with a reference clip (consistent instruct stabilizes cloning; the reference wins conflicts), inline pronunciation control (pinyin / CMU phonemes), and corrects the claim that the default engine can't do voice design — it can, from attributes (#1565)
|
||||
|
||||
### Fixed
|
||||
- Source installs on AMD GPUs honour `OMNIVOICE_TORCH_VARIANT=rocm`: `bun run desktop` now swaps in the ROCm torch wheel after `uv sync` and launches the backend without re-syncing, instead of silently reverting to the CPU-only CUDA build on every start (#1665) — thanks @uberclokr!
|
||||
- `bun run desktop` on a fresh clone no longer fails with "resource path `../../frontend/dist` doesn't exist" — the dev launcher creates the placeholder Tauri resource directory before compiling (#1664) — thanks @uberclokr!
|
||||
- macOS no longer loses TTS after the first request when Python lacks `os.waitid`; subprocess ownership now uses a safe `waitpid` fallback without risking reused process groups (#1656) — thanks @paoloantinori!
|
||||
- Desktop startup, Retry, reset, uninstall, shutdown, and crash recovery now share one backend lifecycle owner; quitting interrupts first-run installers and gracefully drains then force-cleans the full backend process tree, so overlaps cannot duplicate or orphan it (#1635) — thanks @Xohaibxobi!
|
||||
- Large Stories and Audiobook projects now persist in IndexedDB instead of overflowing the `omnivoice.app` localStorage envelope, with quota-safe migration and orderly exit/reload flushing (#1636) — thanks @leodzai!
|
||||
|
||||
@@ -378,9 +378,14 @@ dependency sync — matched to the app's pinned `torch==2.8.0` (the rocm6.2
|
||||
index only ever published up to torch 2.5.1, so it silently failed the
|
||||
reinstall and left the CPU-only CUDA build in place).
|
||||
|
||||
**2. Environment variable (existing installs / headless).** Set
|
||||
**2. Environment variable (existing installs / headless / source).** Set
|
||||
`OMNIVOICE_TORCH_VARIANT=rocm` before launching — the next bootstrap performs
|
||||
the same ROCm reinstall. `OMNIVOICE_TORCH_INDEX=<url>` overrides the wheel
|
||||
the same ROCm reinstall. Source installs honour it too:
|
||||
`OMNIVOICE_TORCH_VARIANT=rocm bun run desktop` swaps torch right after
|
||||
`uv sync` and launches the backend without re-syncing, so the wheel is not
|
||||
reverted on the next start (#1665). Without the variable, `bun run desktop`
|
||||
restores the lockfile's CUDA build — a hand-swapped ROCm wheel does not
|
||||
survive it. `OMNIVOICE_TORCH_INDEX=<url>` overrides the wheel
|
||||
index when you need a different ROCm version — e.g. AMD publishes newer
|
||||
driver-matched builds (7.2.x) at `repo.radeon.com` as a `--find-links` page
|
||||
rather than a PyPI-style index:
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import process from "node:process";
|
||||
|
||||
/** Create Tauri's required dev resource before starting the compiler. */
|
||||
export function launchTauriDev({
|
||||
cwd = process.cwd(),
|
||||
args = process.argv.slice(2),
|
||||
env = process.env,
|
||||
mkdir = mkdirSync,
|
||||
spawn = spawnSync,
|
||||
} = {}) {
|
||||
mkdir(join(cwd, "dist"), { recursive: true });
|
||||
return spawn("bun", ["run", "tauri", "dev", ...args], {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import { join, delimiter } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
import process from "node:process";
|
||||
import { DEV_APP_PROCESS_NAME } from "./desktop-common.mjs";
|
||||
import { launchTauriDev } from "./desktop-dev-launch.mjs";
|
||||
|
||||
/** The env's PATH key — Windows uses "Path", others "PATH"; match case-insensitively. */
|
||||
function pathKeyOf(env) {
|
||||
@@ -114,12 +115,10 @@ if (!cargoResolvable(childEnv)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Run the workspace-local Tauri CLI in dev mode (cwd is already frontend/),
|
||||
// handing it the healed env so its `cargo` spawns inherit the fixed PATH.
|
||||
const res = spawnSync("bun", ["run", "tauri", "dev", ...process.argv.slice(2)], {
|
||||
stdio: "inherit",
|
||||
env: childEnv,
|
||||
});
|
||||
// `frontend/dist` is a required bundle resource even under `tauri dev`.
|
||||
// The helper creates it before spawning Tauri and is behavior-tested with an
|
||||
// injected process runner (#1664). cwd is already frontend/.
|
||||
const res = launchTauriDev({ env: childEnv });
|
||||
if (res.error) {
|
||||
console.error(`❌ failed to launch tauri dev: ${res.error.message}`);
|
||||
process.exit(1);
|
||||
|
||||
+10
-1
@@ -98,8 +98,17 @@ export function buildExitBanner({ code, signal, logTail, logPath, platform = pro
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
// `uv run` re-syncs the venv to uv.lock before launching — which would undo
|
||||
// the opt-in ROCm torch swap `scripts/setup.py` just performed (the lock pins
|
||||
// the CUDA build). `bun run setup:api` already did the sync, so skip it here
|
||||
// whenever the ROCm variant is requested (#1665).
|
||||
export function uvRunArgs(env = process.env) {
|
||||
const rocm = (env.OMNIVOICE_TORCH_VARIANT || "").trim().toLowerCase() === "rocm";
|
||||
return rocm ? [UVICORN_ARGS[0], "--no-sync", ...UVICORN_ARGS.slice(1)] : UVICORN_ARGS;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const child = spawn("uv", UVICORN_ARGS, { stdio: "inherit" });
|
||||
const child = spawn("uv", uvRunArgs(), { stdio: "inherit" });
|
||||
|
||||
// A Ctrl+C / concurrently teardown is a DELIBERATE stop — no scary banner.
|
||||
let interrupted = false;
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
2. **CUDA: cuDNN 8 compat** — Ensures cuDNN 8 libraries are available for
|
||||
CTranslate2 (faster-whisper / WhisperX) alongside PyTorch 2.8+'s cuDNN 9.
|
||||
|
||||
3. **AMD ROCm torch (opt-in)** — with `OMNIVOICE_TORCH_VARIANT=rocm` set,
|
||||
replace the lockfile's CUDA torch build (CPU-only on AMD cards) with the
|
||||
ROCm wheel — the same swap the packaged app's bootstrap performs, so a
|
||||
source install (`bun run desktop`) on an AMD GPU is not stuck on CPU. Runs
|
||||
AFTER `uv sync`, which always restores the locked CUDA build (#1665).
|
||||
|
||||
Run automatically as part of `bun run setup:api` — no user action required.
|
||||
|
||||
Cross-platform:
|
||||
@@ -38,6 +44,55 @@ for _stream in (sys.stdout, sys.stderr):
|
||||
pass
|
||||
|
||||
|
||||
# ── AMD ROCm torch (opt-in) ────────────────────────────────────────────────
|
||||
|
||||
# Keep in sync with ROCM_TORCH_INDEX / rocm_torch_reinstall_args in
|
||||
# frontend/src-tauri/src/bootstrap.rs and [tool.uv.constraint-dependencies].
|
||||
ROCM_TORCH_INDEX = "https://download.pytorch.org/whl/rocm6.4"
|
||||
ROCM_TORCH_PINS = ("torch==2.8.0", "torchaudio==2.8.0", "torchvision==0.23.0")
|
||||
|
||||
|
||||
def _rocm_opt_in(environ=os.environ):
|
||||
"""Return the ROCm wheel index when the user opted in, else None."""
|
||||
if environ.get("OMNIVOICE_TORCH_VARIANT", "").strip().lower() != "rocm":
|
||||
return None
|
||||
return environ.get("OMNIVOICE_TORCH_INDEX") or ROCM_TORCH_INDEX
|
||||
|
||||
|
||||
def _installed_torch_is_rocm():
|
||||
try:
|
||||
import torch # noqa: WPS433 — deliberately lazy; torch is heavy
|
||||
except Exception:
|
||||
return False
|
||||
return bool(getattr(torch.version, "hip", None))
|
||||
|
||||
|
||||
def rocm_torch_reinstall_cmd(index_url, python=None):
|
||||
"""`uv pip install` argv targeting THIS venv (not whatever uv guesses)."""
|
||||
return [
|
||||
"uv", "pip", "install", "--reinstall",
|
||||
"--python", python or sys.executable,
|
||||
*ROCM_TORCH_PINS,
|
||||
"--index-url", index_url,
|
||||
]
|
||||
|
||||
|
||||
def _ensure_rocm_torch():
|
||||
index_url = _rocm_opt_in()
|
||||
if index_url is None:
|
||||
return
|
||||
if _installed_torch_is_rocm():
|
||||
print("✓ ROCm torch already installed")
|
||||
return
|
||||
print(f"⚙ OMNIVOICE_TORCH_VARIANT=rocm — swapping torch to the ROCm wheel ({index_url})")
|
||||
try:
|
||||
subprocess.check_call(rocm_torch_reinstall_cmd(index_url))
|
||||
except (OSError, subprocess.CalledProcessError) as exc:
|
||||
print(f"⚠ ROCm torch install failed ({exc}); keeping the default torch build")
|
||||
return
|
||||
print("✓ ROCm torch installed")
|
||||
|
||||
|
||||
# ── Windows: VC++ Redistributable ─────────────────────────────────────────
|
||||
|
||||
def _ensure_vcredist_windows():
|
||||
@@ -153,6 +208,10 @@ def main():
|
||||
if sys.platform == "darwin":
|
||||
return
|
||||
|
||||
# ── Step 2: opt-in AMD ROCm torch (Linux) ─────────────────────────────
|
||||
if sys.platform.startswith("linux"):
|
||||
_ensure_rocm_torch()
|
||||
|
||||
compat_dir = _find_compat_dir()
|
||||
if compat_dir is None:
|
||||
return
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""`bun run desktop` must create its Tauri resource before spawning (#1664)."""
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parents[1]
|
||||
_LAUNCH = (_ROOT / "scripts" / "desktop-dev-launch.mjs").as_uri()
|
||||
|
||||
|
||||
def test_desktop_dev_creates_dist_before_tauri_dev(tmp_path):
|
||||
cwd = tmp_path / "workspace" / "frontend"
|
||||
script = f"""
|
||||
import {{ launchTauriDev }} from {json.dumps(_LAUNCH)};
|
||||
const calls = [];
|
||||
const result = launchTauriDev({{
|
||||
cwd: {json.dumps(str(cwd))},
|
||||
args: ["--features", "test-feature"],
|
||||
env: {{ PATH: "test-path" }},
|
||||
mkdir: (path, options) => calls.push(["mkdir", path, options]),
|
||||
spawn: (command, args, options) => {{
|
||||
calls.push(["spawn", command, args, options]);
|
||||
return {{ status: 0 }};
|
||||
}},
|
||||
}});
|
||||
console.log(JSON.stringify({{ calls, result }}));
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
["node", "--input-type=module", "--eval", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
observed = json.loads(completed.stdout)
|
||||
|
||||
assert observed == {
|
||||
"calls": [
|
||||
["mkdir", str(cwd / "dist"), {"recursive": True}],
|
||||
[
|
||||
"spawn",
|
||||
"bun",
|
||||
["run", "tauri", "dev", "--features", "test-feature"],
|
||||
{"stdio": "inherit", "env": {"PATH": "test-path"}},
|
||||
],
|
||||
],
|
||||
"result": {"status": 0},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Source installs must honour `OMNIVOICE_TORCH_VARIANT=rocm` (#1665).
|
||||
|
||||
`uv sync` always restores the lockfile's CUDA torch build, which is CPU-only
|
||||
on AMD cards, and `uv run` re-syncs before every launch — so a hand-swapped
|
||||
ROCm wheel was silently reverted by the next `bun run desktop`. The packaged
|
||||
app's bootstrap (`bootstrap.rs`) already performs the swap on opt-in; these
|
||||
tests pin the dev-flow equivalents: `scripts/setup.py` reinstalls the ROCm
|
||||
wheel after the sync, and `scripts/dev-backend.mjs` launches with
|
||||
`uv run --no-sync` so it sticks.
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_SETUP = os.path.join(_ROOT, "scripts", "setup.py")
|
||||
_DEV_BACKEND = os.path.join(_ROOT, "scripts", "dev-backend.mjs")
|
||||
_BOOTSTRAP = os.path.join(_ROOT, "frontend", "src-tauri", "src", "bootstrap.rs")
|
||||
|
||||
|
||||
def _load_setup():
|
||||
spec = importlib.util.spec_from_file_location("vs_setup", _SETUP)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
def test_rocm_opt_in_requires_explicit_variant():
|
||||
setup = _load_setup()
|
||||
assert setup._rocm_opt_in({}) is None
|
||||
assert setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "auto"}) is None
|
||||
assert setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "ROCm"}) == setup.ROCM_TORCH_INDEX
|
||||
assert (
|
||||
setup._rocm_opt_in({"OMNIVOICE_TORCH_VARIANT": "rocm", "OMNIVOICE_TORCH_INDEX": "https://x/"})
|
||||
== "https://x/"
|
||||
)
|
||||
|
||||
|
||||
def test_rocm_reinstall_targets_this_venv_with_bootstrap_pins():
|
||||
setup = _load_setup()
|
||||
cmd = setup.rocm_torch_reinstall_cmd("https://idx/", python="/venv/bin/python")
|
||||
assert cmd == [
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"--reinstall",
|
||||
"--python",
|
||||
"/venv/bin/python",
|
||||
*setup.ROCM_TORCH_PINS,
|
||||
"--index-url",
|
||||
"https://idx/",
|
||||
]
|
||||
# Same pins + default index as the packaged app's bootstrap.
|
||||
rs = open(_BOOTSTRAP, encoding="utf-8").read()
|
||||
for pin in setup.ROCM_TORCH_PINS:
|
||||
assert f'"{pin}"' in rs, f"{pin} drifted from bootstrap.rs"
|
||||
assert f'"{setup.ROCM_TORCH_INDEX}"' in rs
|
||||
|
||||
|
||||
def test_dev_backend_skips_resync_when_rocm_requested():
|
||||
module_uri = Path(_DEV_BACKEND).as_uri()
|
||||
script = f"""
|
||||
const mod = await import({json.dumps(module_uri)});
|
||||
console.log(JSON.stringify({{
|
||||
base: mod.UVICORN_ARGS,
|
||||
unset: mod.uvRunArgs({{}}),
|
||||
auto: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: "auto" }}),
|
||||
rocm: mod.uvRunArgs({{ OMNIVOICE_TORCH_VARIANT: " ROCm " }}),
|
||||
}}));
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
["node", "--input-type=module", "--eval", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
observed = json.loads(completed.stdout)
|
||||
base = observed["base"]
|
||||
assert observed["unset"] == base
|
||||
assert observed["auto"] == base
|
||||
assert observed["rocm"] == [base[0], "--no-sync", *base[1:]]
|
||||
Reference in New Issue
Block a user