diff --git a/.gitignore b/.gitignore index 38c3cc35..6dc0dc1f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ htmlcov/ data/ jobs/ .run/ +.build # Local env / secrets .env diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..b37ebefb --- /dev/null +++ b/.trivyignore @@ -0,0 +1,9 @@ +# CVE-2025-32434: torch remote code execution via torch.load, fixed in 2.6.0. +# The Intel macOS (x86_64) runtime is pinned to torch>=2.2,<2.3 because +# PyTorch does not publish macOS x86_64 wheels for 2.6.x. StemDeck never +# calls torch.load() on untrusted input; all model weights are fetched by +# Demucs from its own trusted cache. Risk on a local single-user app with +# no network-facing torch.load path is negligible. +# Drop this ignore once PyTorch publishes 2.6.x macOS x86_64 wheels or once +# the torchaudio/torchcodec story stabilises and the pin can be lifted. +CVE-2025-32434 diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml index ce8d72fa..c731e087 100644 --- a/.woodpecker/ci.yml +++ b/.woodpecker/ci.yml @@ -78,6 +78,7 @@ steps: --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed + --ignorefile .trivyignore --skip-dirs .venv --skip-dirs jobs . diff --git a/.woodpecker/macos-release.yml b/.woodpecker/macos-release.yml new file mode 100644 index 00000000..e3a3a2ad --- /dev/null +++ b/.woodpecker/macos-release.yml @@ -0,0 +1,149 @@ +when: + - event: release + - event: tag + +labels: + platform: darwin/arm64 + backend: local + +steps: + prepare-macos-version: + image: bash + commands: + - | + if [ -z "${CI_COMMIT_TAG:-}" ]; then + echo "CI_COMMIT_TAG is not set" >&2 + exit 1 + fi + VERSION="${CI_COMMIT_TAG#v}" + printf '{ "version": "%s" }\n' "$VERSION" > static/version.json + echo "VERSION=$VERSION" > .macos-release.env + echo "Wrote static/version.json for $VERSION" + + build-macos-arm64: + image: bash + depends_on: + - prepare-macos-version + commands: + - . ./.macos-release.env + - command -v zstd + - scripts/macos/make-iconset.sh + - ARCH=arm64 VERSION="$VERSION" scripts/macos/make-runtime-pack.sh + - ARCH=arm64 VERSION="$VERSION" scripts/macos/make-app.sh + - ARCH=arm64 VERSION="$VERSION" scripts/macos/make-dmg.sh + + build-macos-x64: + image: bash + depends_on: + - prepare-macos-version + commands: + - . ./.macos-release.env + - command -v zstd + - | + if ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then + echo "ERROR: Rosetta 2 is required to build the x64 runtime on this agent." >&2 + exit 1 + fi + - | + if ! command -v rustup >/dev/null 2>&1 || ! rustup target list --installed | grep -q x86_64-apple-darwin; then + echo "ERROR: x86_64-apple-darwin Rust target not installed. Run: rustup target add x86_64-apple-darwin" >&2 + exit 1 + fi + - ARCH=x64 VERSION="$VERSION" scripts/macos/make-runtime-pack.sh + - ARCH=x64 VERSION="$VERSION" scripts/macos/make-app.sh + - ARCH=x64 VERSION="$VERSION" scripts/macos/make-dmg.sh + + inspect-macos-artifacts: + image: bash + depends_on: + - build-macos-arm64 + - build-macos-x64 + commands: + - test -f .build/macos-dist/StemDeck-macOS-arm64.dmg + - test -f .build/StemDeck-runtime-macOS-arm64.tar.zst + - test -f .build/macos-dist/SHA256SUMS-macOS-arm64.txt + - test -f .build/macos-dist/StemDeck-macOS-x64.dmg + - test -f .build/StemDeck-runtime-macOS-x64.tar.zst + - test -f .build/macos-dist/SHA256SUMS-macOS-x64.txt + - cat .build/macos-dist/SHA256SUMS-macOS-arm64.txt + - cat .build/macos-dist/SHA256SUMS-macOS-x64.txt + - du -sh .build/macos-dist/StemDeck-macOS-arm64.dmg .build/StemDeck-runtime-macOS-arm64.tar.zst + - du -sh .build/macos-dist/StemDeck-macOS-x64.dmg .build/StemDeck-runtime-macOS-x64.tar.zst + - | + if find desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app \ + \( -iname '*python*' -o -iname '*torch*' -o -iname '*ffmpeg*' -o -iname '*ffprobe*' \) | + grep -q .; then + echo "arm64 StemDeck.app contains runtime binaries that should stay outside the DMG." >&2 + exit 1 + fi + - | + if find desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/StemDeck.app \ + \( -iname '*python*' -o -iname '*torch*' -o -iname '*ffmpeg*' -o -iname '*ffprobe*' \) | + grep -q .; then + echo "x64 StemDeck.app contains runtime binaries that should stay outside the DMG." >&2 + exit 1 + fi + - | + for arch in arm64 x64; do + mountpoint="$(mktemp -d /tmp/stemdeck-dmg.XXXXXX)" + hdiutil attach ".build/macos-dist/StemDeck-macOS-${arch}.dmg" -readonly -nobrowse -mountpoint "$mountpoint" + trap 'hdiutil detach "$mountpoint" >/dev/null 2>&1 || true; rmdir "$mountpoint" >/dev/null 2>&1 || true' EXIT + test -d "$mountpoint/StemDeck.app" + test -L "$mountpoint/Applications" + test -f "$mountpoint/README-macOS.txt" + test -f "$mountpoint/THIRD_PARTY_NOTICES.txt" + hdiutil detach "$mountpoint" + rmdir "$mountpoint" + trap - EXIT + done + + upload-macos-artifacts: + image: bash + when: + - event: release + depends_on: + - inspect-macos-artifacts + environment: + GH_TOKEN: + from_secret: github_token + commands: + - | + if [ -z "${CI_COMMIT_TAG:-}" ]; then + echo "CI_COMMIT_TAG is required to upload release assets." >&2 + exit 1 + fi + - | + if ! command -v gh >/dev/null 2>&1; then + echo "GitHub CLI is required on the macOS agent to upload release assets." >&2 + exit 1 + fi + - | + scan_note="macOS arm64 and x64 DMGs and runtime packs were built and inspected on a macOS Woodpecker agent before upload." + body="$(gh release view "$CI_COMMIT_TAG" --repo "thcp/stemdeck" --json body --jq ".body")" + case "$body" in + *"$scan_note"*) ;; + *) + notes_path="$(mktemp /tmp/stemdeck-release-notes.XXXXXX.md)" + { + printf '%s\n\n' "$body" + printf '### Artifact build\n\n' + printf -- '- %s\n' "$scan_note" + } > "$notes_path" + gh release edit "$CI_COMMIT_TAG" --repo "thcp/stemdeck" --notes-file "$notes_path" + rm -f "$notes_path" + ;; + esac + - | + existing=$(gh release view "$CI_COMMIT_TAG" --repo "thcp/stemdeck" --json assets --jq '[.assets[].name] | join(" ")') + if echo "$existing" | grep -q "StemDeck-macOS-arm64.dmg"; then + echo "macOS assets already present for $CI_COMMIT_TAG — skipping upload (pre-release promotion)." + exit 0 + fi + gh release upload "$CI_COMMIT_TAG" + ".build/macos-dist/StemDeck-macOS-arm64.dmg" + ".build/StemDeck-runtime-macOS-arm64.tar.zst" + ".build/macos-dist/SHA256SUMS-macOS-arm64.txt" + ".build/macos-dist/StemDeck-macOS-x64.dmg" + ".build/StemDeck-runtime-macOS-x64.tar.zst" + ".build/macos-dist/SHA256SUMS-macOS-x64.txt" + --repo "thcp/stemdeck" diff --git a/.woodpecker/windows-release.yml b/.woodpecker/windows-release.yml index 20698ec6..0995b61b 100644 --- a/.woodpecker/windows-release.yml +++ b/.woodpecker/windows-release.yml @@ -1,5 +1,6 @@ when: - event: release + - event: tag labels: platform: windows/amd64 @@ -98,10 +99,15 @@ steps: Set-Content -Path $notesPath -Value $updatedBody -Encoding UTF8 gh release edit "$env:CI_COMMIT_TAG" --repo "thcp/stemdeck" --notes-file $notesPath } - - gh release upload "$env:CI_COMMIT_TAG" + - | + $existing = gh release view "$env:CI_COMMIT_TAG" --repo "thcp/stemdeck" --json assets --jq '[.assets[].name] | join(" ")' + if ($existing -like "*StemDeck-Windows-x64.zip*") { + Write-Host "Windows assets already present for $env:CI_COMMIT_TAG — skipping upload (pre-release promotion)." + exit 0 + } + gh release upload "$env:CI_COMMIT_TAG" "dist/StemDeck-Windows-x64.NVIDIA.zip" "dist/StemDeck-Windows-x64.NVIDIA.zip.sha256" "dist/StemDeck-Windows-x64.zip" "dist/StemDeck-Windows-x64.zip.sha256" --repo "thcp/stemdeck" - --clobber diff --git a/README.md b/README.md index 06ee049b..659e5e66 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@
-![StemDeck](stemdeck.png) - -# StemDeck +StemDeck **Free, local stem separation. No account. No upload. No subscription.** @@ -84,7 +82,20 @@ If you need speed, quality, mobile access, or the extra musician tooling, the co ## Download -Pre-built portable zips are attached to each [GitHub Release](https://github.com/thcp/stemdeck/releases). No installer needed; extract and run. +Pre-built installers and zips are attached to each [GitHub Release](https://github.com/thcp/stemdeck/releases). + +**macOS** + +| DMG | GPU | Chip | +|---|---|---| +| `StemDeck-macOS-arm64.dmg` | Apple Silicon (MPS) | M1 and later | +| `StemDeck-macOS-x64.dmg` | CPU only | Intel | + +Open the DMG, drag StemDeck to Applications, and launch it. On first launch the setup screen downloads the Python runtime (~500 MB), FFmpeg, and the Demucs model (~170 MB). Subsequent launches skip setup and start in seconds. No Python or system dependencies required. + +macOS may show a Gatekeeper prompt on first open — right-click the app and choose Open to bypass it. + +**Windows** | Zip | GPU | Approx. size | |---|---|---| @@ -97,7 +108,7 @@ Extract the zip anywhere, run `StemDeck.exe`. On first launch the app verifies t ## Technologies -StemDeck is built on **[Python 3.10+](https://python.org)** managed via **[uv](https://github.com/astral-sh/uv)**, with a **[FastAPI](https://fastapi.tiangolo.com)** backend serving REST and Server-Sent Events. Stem separation uses **[Demucs](https://github.com/facebookresearch/demucs)** (`htdemucs_6s`), Meta AI's open-source 6-stem neural network. YouTube audio is fetched via **[yt-dlp](https://github.com/yt-dlp/yt-dlp)**; transcoding and mixing use **[FFmpeg](https://ffmpeg.org)**. BPM detection and key analysis run on **[librosa](https://librosa.org)**; loudness measurement uses **[pyloudnorm](https://github.com/csteinmetz1/pyloudnorm)** (ITU-R BS.1770). The Windows desktop shell is **[Tauri v2](https://tauri.app)** (Rust/WebView2). The frontend is vanilla JS with the Web Audio API, no framework and no build step; waveforms are rendered on `` using min/max sample rendering. +StemDeck is built on **[Python 3.10+](https://python.org)** managed via **[uv](https://github.com/astral-sh/uv)**, with a **[FastAPI](https://fastapi.tiangolo.com)** backend serving REST and Server-Sent Events. Stem separation uses **[Demucs](https://github.com/facebookresearch/demucs)** (`htdemucs_6s`), Meta AI's open-source 6-stem neural network. YouTube audio is fetched via **[yt-dlp](https://github.com/yt-dlp/yt-dlp)**; transcoding and mixing use **[FFmpeg](https://ffmpeg.org)**. BPM detection and key analysis run on **[librosa](https://librosa.org)**; loudness measurement uses **[pyloudnorm](https://github.com/csteinmetz1/pyloudnorm)** (ITU-R BS.1770). The macOS and Windows desktop shells are **[Tauri v2](https://tauri.app)** (Rust/WKWebView on macOS, Rust/WebView2 on Windows). The frontend is vanilla JS with the Web Audio API, no framework and no build step; waveforms are rendered on `` using min/max sample rendering. *Thanks to the creators and maintainers of all the open-source libraries that make StemDeck possible.* @@ -105,13 +116,45 @@ StemDeck is built on **[Python 3.10+](https://python.org)** managed via **[uv](h ## Build from Source -*(macOS / Linux / Windows with Python 3.10+)* +### macOS Native App + +Requires Rust, Node.js, and Python 3.10–3.13. Builds a self-contained `.app` that downloads its own runtime on first launch. + +```sh +# First time only — add the cross-compilation targets +rustup target add aarch64-apple-darwin # Apple Silicon +rustup target add x86_64-apple-darwin # Intel + +# Build Apple Silicon +ARCH=arm64 scripts/macos/make-runtime-pack.sh +ARCH=arm64 scripts/macos/make-app.sh +ARCH=arm64 scripts/macos/make-dmg.sh + +# Build Intel (requires Rosetta 2 and an x86_64 Python) +ARCH=x64 scripts/macos/make-runtime-pack.sh +ARCH=x64 scripts/macos/make-app.sh +ARCH=x64 scripts/macos/make-dmg.sh +``` + +The `.app` lands at `desktop/src-tauri/target//release/bundle/macos/StemDeck.app`. The DMG lands at `.build/macos-dist/StemDeck-macOS-.dmg`. + +To run a fresh build directly without the DMG: + +```sh +# Wipe previous app data, then open +rm -rf ~/Library/Application\ Support/StemDeck +open desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app +``` + +--- + +### Web Server (macOS / Linux / Windows with Python 3.10+) -### Prerequisites +#### Prerequisites Python 3.10 or newer, `ffmpeg` on your PATH, and [uv](https://github.com/astral-sh/uv). Around 170 MB of free disk for the Demucs model, which downloads automatically on first run. -### macOS / Linux (one-shot) +#### macOS / Linux (one-shot) ```sh git clone https://github.com/thcp/stemdeck stemdeck && cd stemdeck @@ -123,7 +166,7 @@ Open . `setup` uses Homebrew on macOS and `apt-get` on Debian/Ubuntu. For other Linux distros, install `ffmpeg` and [uv](https://github.com/astral-sh/uv) manually, then run `uv sync` followed by `./run.sh start`. -### Manual (any platform) +#### Manual (any platform) ```sh git clone https://github.com/thcp/stemdeck stemdeck && cd stemdeck @@ -131,7 +174,7 @@ uv sync uv run uvicorn app.main:app --reload ``` -### Docker +#### Docker ```sh docker compose -f build/docker-compose.yml up --build @@ -139,7 +182,7 @@ docker compose -f build/docker-compose.yml up --build Stems land in `./jobs/` on the host. Demucs weights are cached in a named volume so they don't re-download on rebuild. Note: no GPU passthrough on macOS Docker. -### `run.sh` control script +#### `run.sh` control script ```sh ./run.sh setup # one-shot: install ffmpeg + uv, then uv sync diff --git a/app/main.py b/app/main.py index 7cfeb99f..3067e374 100644 --- a/app/main.py +++ b/app/main.py @@ -3,6 +3,7 @@ import asyncio import json import logging +import os from collections.abc import AsyncIterator from contextlib import asynccontextmanager from importlib.metadata import PackageNotFoundError @@ -69,9 +70,32 @@ async def _sweep_loop() -> None: await asyncio.sleep(3600) +async def _desktop_parent_watchdog(parent_pid: int) -> None: + while True: + try: + os.kill(parent_pid, 0) + except ProcessLookupError: + _log.info("desktop parent process exited; stopping backend") + os._exit(0) + except PermissionError: + _log.warning("desktop parent process check denied; stopping backend") + os._exit(0) + await asyncio.sleep(1) + + @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: asyncio.create_task(_sweep_loop()) + if os.environ.get("STEMDECK_DESKTOP") == "1": + parent_pid = os.environ.get("STEMDECK_PARENT_PID") + if parent_pid: + try: + parent_pid_int = int(parent_pid) + except ValueError: + _log.warning("invalid STEMDECK_PARENT_PID=%r", parent_pid) + else: + if parent_pid_int > 0 and parent_pid_int != os.getpid(): + asyncio.create_task(_desktop_parent_watchdog(parent_pid_int)) yield diff --git a/desktop/package.json b/desktop/package.json index 68f05a2c..3b3c5445 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "stemdeck-desktop", - "version": "0.1.0-alpha.0", + "version": "0.4.0-alpha.1", "private": true, "type": "module", "scripts": { diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index c0f12e56..f0994bd0 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -309,6 +309,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -1012,8 +1018,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1023,9 +1031,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 5.3.0", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1297,6 +1307,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -1710,6 +1736,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "markup5ever" version = "0.38.0" @@ -2150,7 +2182,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand", + "rand 0.8.6", ] [[package]] @@ -2273,6 +2305,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "precomputed-hash" version = "0.1.1" @@ -2360,6 +2401,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.45" @@ -2387,7 +2483,27 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -2396,6 +2512,15 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -2471,6 +2596,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.3" @@ -2505,6 +2668,20 @@ dependencies = [ "web-sys", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2520,12 +2697,53 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -2716,6 +2934,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.19.0" @@ -2885,10 +3115,12 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "stemdeck" -version = "0.1.0-alpha.0" +version = "0.4.0-alpha.1" dependencies = [ + "reqwest 0.12.28", "serde", "serde_json", + "sha2", "tauri", "tauri-build", "zip", @@ -2924,6 +3156,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swift-rs" version = "1.0.7" @@ -3076,7 +3314,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.3", "serde", "serde_json", "serde_repr", @@ -3350,6 +3588,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.2" @@ -3364,6 +3617,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -3660,6 +3923,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -3896,6 +4165,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web_atoms" version = "0.2.4" @@ -3952,6 +4231,15 @@ dependencies = [ "system-deps", ] +[[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" @@ -4182,6 +4470,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -4561,6 +4858,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "zerofrom" version = "0.1.7" @@ -4582,6 +4899,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index c4208fbb..793d8f54 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "stemdeck" -version = "0.3.0-alpha.1" +version = "0.4.0-alpha.1" description = "StemDeck desktop launcher" authors = ["StemDeck contributors"] edition = "2021" @@ -10,7 +10,9 @@ rust-version = "1.88" tauri-build = { version = "2", features = [] } [dependencies] +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.10" tauri = { version = "2", features = [] } zip = { version = "2", default-features = false, features = ["deflate"] } diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..6653ee4f Binary files /dev/null and b/desktop/src-tauri/icons/icon.icns differ diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png index 402ef444..9307d054 100644 Binary files a/desktop/src-tauri/icons/icon.png and b/desktop/src-tauri/icons/icon.png differ diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs index a89c93f0..8b7fa8fb 100644 --- a/desktop/src-tauri/src/main.rs +++ b/desktop/src-tauri/src/main.rs @@ -1,4 +1,5 @@ -use serde::Serialize; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; use std::{ env, fs, io::{Read, Write}, @@ -9,13 +10,15 @@ use std::{ thread, time::{Duration, Instant, SystemTime, UNIX_EPOCH}, }; -use tauri::Manager; +use tauri::{Emitter, Manager}; #[cfg(windows)] -use {std::fs::File, zip::ZipArchive}; +use zip::ZipArchive; const SETUP_VERSION: u64 = 1; const DEFAULT_WINDOWS_FFMPEG_URL: &str = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"; +const DEFAULT_MACOS_FFMPEG_URL: &str = "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip"; +const DEFAULT_MACOS_FFPROBE_URL: &str = "https://evermeet.cx/ffmpeg/getrelease/ffprobe/zip"; #[derive(Default)] struct BackendState { @@ -36,6 +39,47 @@ struct RuntimeProbe { torch_device: Option, } +#[derive(Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +struct RuntimeManifest { + version: String, + arch: String, + runtime_url: String, + runtime_sha256: String, + runtime_size: Option, + archive_name: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct RuntimePackStatus { + manifest_ready: bool, + manifest_path: Option, + runtime_ready: bool, + runtime_dir: String, + backend_ready: bool, + python_ready: bool, + archive_path: Option, + archive_ready: bool, + installed_version: Option, + manifest: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct RuntimeArchive { + archive_path: String, + sha256: String, + size: u64, +} + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct DownloadProgress { + received: u64, + total: Option, +} + #[derive(Serialize)] struct BackendStarted { url: String, @@ -65,6 +109,10 @@ fn main() { .invoke_handler(tauri::generate_handler![ probe_runtime, ensure_workspace, + runtime_pack_status, + download_runtime_pack, + verify_runtime_pack, + extract_runtime_pack, ensure_external_assets, ensure_torch_device, start_backend, @@ -72,11 +120,20 @@ fn main() { ]) .build(tauri::generate_context!()) .expect("failed to build StemDeck desktop app") - .run(|app_handle, event| { - if let tauri::RunEvent::ExitRequested { .. } = event { + .run(|app_handle, event| match event { + tauri::RunEvent::ExitRequested { .. } => { let state = app_handle.state::(); stop_backend(&state); } + tauri::RunEvent::WindowEvent { + event: tauri::WindowEvent::CloseRequested { .. }, + .. + } => { + let state = app_handle.state::(); + stop_backend(&state); + app_handle.exit(0); + } + _ => {} }); } @@ -108,13 +165,132 @@ fn read_config_str(data_dir: &std::path::Path, key: &str) -> Option { value.get(key)?.as_str().map(|s| s.to_string()) } +#[tauri::command] +fn runtime_pack_status() -> Result { + let root = app_root()?; + let data_dir = local_data_dir()?; + let runtime_dir = runtime_dir(&data_dir); + let backend_dir = runtime_dir.join("backend"); + let python = runtime_python_path(&data_dir); + let manifest_path = runtime_manifest_path(&root); + let manifest = manifest_path + .as_deref() + .and_then(|path| read_runtime_manifest(path).ok()); + let archive_path = manifest + .as_ref() + .map(|item| runtime_archive_path(&data_dir, item)); + let installed_version = read_runtime_install_manifest(&runtime_dir) + .and_then(|value| value.get("version")?.as_str().map(|text| text.to_string())); + + Ok(RuntimePackStatus { + manifest_ready: manifest.is_some(), + manifest_path: manifest_path.map(|path| path.display().to_string()), + runtime_ready: backend_dir.join("app").is_dir() && python.is_file(), + runtime_dir: runtime_dir.display().to_string(), + backend_ready: backend_dir.join("app").is_dir(), + python_ready: python.is_file(), + archive_ready: archive_path.as_ref().is_some_and(|path| path.is_file()), + archive_path: archive_path.map(|path| path.display().to_string()), + installed_version, + manifest, + }) +} + +#[tauri::command] +async fn download_runtime_pack(app_handle: tauri::AppHandle) -> Result { + ensure_workspace()?; + let root = app_root()?; + let data_dir = local_data_dir()?; + let manifest = load_runtime_manifest(&root)?; + validate_runtime_manifest(&manifest)?; + let archive = runtime_archive_path(&data_dir, &manifest); + if let Some(parent) = archive.parent() { + fs::create_dir_all(parent) + .map_err(|e| format!("failed to create {}: {e}", parent.display()))?; + } + download_file_with_progress(&manifest.runtime_url, &archive, &app_handle).await?; + verify_runtime_archive(&manifest, &archive) +} + +#[tauri::command] +fn verify_runtime_pack() -> Result { + let root = app_root()?; + let data_dir = local_data_dir()?; + let manifest = load_runtime_manifest(&root)?; + validate_runtime_manifest(&manifest)?; + let archive = runtime_archive_path(&data_dir, &manifest); + verify_runtime_archive(&manifest, &archive) +} + +#[tauri::command] +fn extract_runtime_pack() -> Result { + ensure_workspace()?; + let root = app_root()?; + let data_dir = local_data_dir()?; + let manifest = load_runtime_manifest(&root)?; + validate_runtime_manifest(&manifest)?; + let archive = runtime_archive_path(&data_dir, &manifest); + verify_runtime_archive(&manifest, &archive)?; + + let runtime = runtime_dir(&data_dir); + let tmp = data_dir.join("runtime.tmp"); + let old = data_dir.join("runtime.old"); + if tmp.exists() { + fs::remove_dir_all(&tmp).map_err(|e| format!("failed to remove {}: {e}", tmp.display()))?; + } + fs::create_dir_all(&tmp).map_err(|e| format!("failed to create {}: {e}", tmp.display()))?; + + extract_tar_archive(&archive, &tmp)?; + let extracted = tmp.join("runtime"); + if !extracted.join("backend").join("app").is_dir() { + return Err("runtime archive did not contain runtime/backend/app".to_string()); + } + if !extracted + .join("python") + .join("bin") + .join("python") + .is_file() + { + return Err("runtime archive did not contain runtime/python/bin/python".to_string()); + } + + let install_manifest = serde_json::json!({ + "version": manifest.version, + "arch": manifest.arch, + "runtimeUrl": manifest.runtime_url, + "runtimeSha256": manifest.runtime_sha256, + "installedAt": unix_timestamp(), + }); + fs::write( + extracted.join("runtime-manifest.json"), + serde_json::to_string_pretty(&install_manifest) + .map_err(|e| format!("failed to serialize runtime install manifest: {e}"))? + + "\n", + ) + .map_err(|e| format!("failed to write runtime manifest: {e}"))?; + + if old.exists() { + fs::remove_dir_all(&old).map_err(|e| format!("failed to remove {}: {e}", old.display()))?; + } + if runtime.exists() { + fs::rename(&runtime, &old) + .map_err(|e| format!("failed to move existing runtime aside: {e}"))?; + } + fs::rename(&extracted, &runtime).map_err(|e| format!("failed to install runtime: {e}"))?; + let _ = fs::remove_dir_all(&tmp); + let _ = fs::remove_dir_all(&old); + + let python = runtime.join("python").join("bin").join("python"); + patch_pyvenv_cfg(&python); + runtime_pack_status() +} + #[tauri::command] fn ensure_workspace() -> Result<(), String> { let root = app_root()?; let data = local_data_dir()?; migrate_legacy_data(&root, &data); - fs::create_dir_all(&data) - .map_err(|e| format!("failed to create data dir: {e}"))?; + fs::create_dir_all(&data).map_err(|e| format!("failed to create data dir: {e}"))?; for dir in ["cache", "downloads", "ffmpeg", "jobs", "logs", "models"] { fs::create_dir_all(data.join(dir)) .map_err(|e| format!("failed to create data/{dir}: {e}"))?; @@ -168,7 +344,7 @@ fn start_backend(state: tauri::State) -> Result) -> Result Result { .ok_or_else(|| "Python not found".to_string())?; patch_pyvenv_cfg(&python); - let setup = match detect_nvidia_gpu() { - Some((gpu_name, cuda_version)) => { - let index_url = cuda_index_url(&cuda_version); - install_cuda_torch(&python, &index_url)?; - let cuda_verified = verify_cuda_torch(&python); - GpuSetup { - gpu_detected: true, - gpu_name: Some(gpu_name), - cuda_version: Some(cuda_version), - torch_device: if cuda_verified { "cuda" } else { "cpu" }.to_string(), - cuda_verified, - } - } - None => GpuSetup { - gpu_detected: false, - gpu_name: None, + #[cfg(target_os = "macos")] + { + let mps_available = verify_mps_torch(&python); + let device = if mps_available { "mps" } else { "cpu" }; + persist_torch_device(&data_dir, device); + Ok(GpuSetup { + gpu_detected: mps_available, + gpu_name: if mps_available { + Some("Apple Silicon (MPS)".to_string()) + } else { + None + }, cuda_version: None, - torch_device: "cpu".to_string(), + torch_device: device.to_string(), cuda_verified: false, - }, - }; - // Persist so subsequent launches skip this step entirely. - persist_torch_device(&data_dir, &setup.torch_device); - Ok(setup) + }) + } + + #[cfg(not(target_os = "macos"))] + { + let setup = match detect_nvidia_gpu() { + Some((gpu_name, cuda_version)) => { + let index_url = cuda_index_url(&cuda_version); + install_cuda_torch(&python, &index_url)?; + let cuda_verified = verify_cuda_torch(&python); + GpuSetup { + gpu_detected: true, + gpu_name: Some(gpu_name), + cuda_version: Some(cuda_version), + torch_device: if cuda_verified { "cuda" } else { "cpu" }.to_string(), + cuda_verified, + } + } + None => GpuSetup { + gpu_detected: false, + gpu_name: None, + cuda_version: None, + torch_device: "cpu".to_string(), + cuda_verified: false, + }, + }; + // Persist so subsequent launches skip this step entirely. + persist_torch_device(&data_dir, &setup.torch_device); + Ok(setup) + } } fn is_cpu_only_package(root: &Path, data_dir: &Path) -> bool { @@ -282,6 +480,21 @@ fn persist_torch_device(data_dir: &std::path::Path, device: &str) { ); } +#[cfg(target_os = "macos")] +fn verify_mps_torch(python: &Path) -> bool { + Command::new(python) + .args([ + "-c", + "import torch; exit(0 if getattr(torch.backends, 'mps', None) and torch.backends.mps.is_available() else 1)", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .map(|s| s.success()) + .unwrap_or(false) +} + +#[cfg(not(target_os = "macos"))] fn nvidia_smi_exe() -> &'static str { // nvidia-smi.exe lives in System32 on Windows but Tauri child processes // inherit a stripped PATH that may not include it. @@ -295,6 +508,7 @@ fn nvidia_smi_exe() -> &'static str { "nvidia-smi" } +#[cfg(not(target_os = "macos"))] fn detect_nvidia_gpu() -> Option<(String, String)> { let smi = nvidia_smi_exe(); let mut cmd = Command::new(smi); @@ -323,6 +537,7 @@ fn detect_nvidia_gpu() -> Option<(String, String)> { Some((gpu_name, cuda_version)) } +#[cfg(not(target_os = "macos"))] fn parse_cuda_version(smi_output: &str) -> Option { for line in smi_output.lines() { if let Some(pos) = line.find("CUDA Version:") { @@ -341,6 +556,7 @@ fn parse_cuda_version(smi_output: &str) -> Option { None } +#[cfg(not(target_os = "macos"))] fn cuda_tag(cuda_version: &str) -> &'static str { let parts: Vec = cuda_version .splitn(2, '.') @@ -354,6 +570,7 @@ fn cuda_tag(cuda_version: &str) -> &'static str { } } +#[cfg(not(target_os = "macos"))] fn cuda_index_url(cuda_version: &str) -> String { format!( "https://download.pytorch.org/whl/{}", @@ -361,6 +578,7 @@ fn cuda_index_url(cuda_version: &str) -> String { ) } +#[cfg(not(target_os = "macos"))] fn cuda_tag_from_url(index_url: &str) -> &str { index_url.rsplit('/').next().unwrap_or("cu124") } @@ -369,18 +587,20 @@ fn cuda_tag_from_url(index_url: &str) -> &str { /// this file before Python starts, so stale build-machine paths can prevent the /// backend from emitting any log output at all. fn patch_pyvenv_cfg(python: &Path) { - let Some(scripts_dir) = python.parent() else { + let Some(bin_dir) = python.parent() else { return; }; - let Some(venv_root) = scripts_dir.parent() else { + let Some(venv_root) = bin_dir.parent() else { return; }; - let bundled_python = venv_root.join(if cfg!(windows) { - "python.exe" - } else { - "python" - }); - if !bundled_python.is_file() || !venv_root.join("Lib").join("os.py").is_file() { + let bundled_python = venv_root + .join(if cfg!(windows) { "Scripts" } else { "bin" }) + .join(if cfg!(windows) { + "python.exe" + } else { + "python" + }); + if !bundled_python.is_file() || !python_stdlib_present(venv_root) { return; } let cfg_path = venv_root.join("pyvenv.cfg"); @@ -413,6 +633,20 @@ fn patch_pyvenv_cfg(python: &Path) { let _ = fs::write(&cfg_path, patched); } +fn python_stdlib_present(venv_root: &Path) -> bool { + if venv_root.join("Lib").join("os.py").is_file() { + return true; + } + let lib = venv_root.join("lib"); + let Ok(entries) = fs::read_dir(lib) else { + return false; + }; + entries + .filter_map(Result::ok) + .any(|entry| entry.path().join("os.py").is_file()) +} + +#[cfg(not(target_os = "macos"))] fn install_cuda_torch(python: &Path, index_url: &str) -> Result<(), String> { // Skip only when CUDA torch is already active — torch.version.cuda is // None for CPU-only wheels, so this correctly re-installs when needed. @@ -459,6 +693,7 @@ fn install_cuda_torch(python: &Path, index_url: &str) -> Result<(), String> { } } +#[cfg(not(target_os = "macos"))] fn verify_cuda_torch(python: &Path) -> bool { Command::new(python) .args([ @@ -482,7 +717,14 @@ fn open_url(url: String) -> Result<(), String> { cmd.spawn() .map_err(|e| format!("failed to open URL: {e}"))?; } - #[cfg(not(windows))] + #[cfg(target_os = "macos")] + { + Command::new("open") + .arg(&url) + .spawn() + .map_err(|e| format!("failed to open URL: {e}"))?; + } + #[cfg(all(unix, not(target_os = "macos")))] { Command::new("xdg-open") .arg(&url) @@ -504,7 +746,8 @@ fn stop_backend(state: &BackendState) { /// Returns the persistent user data directory for StemDeck. /// On Windows: %LocalAppData%\StemDeck -/// On Linux/macOS: $XDG_DATA_HOME/stemdeck or ~/.local/share/stemdeck +/// On macOS: ~/Library/Application Support/StemDeck +/// On Linux: $XDG_DATA_HOME/stemdeck or ~/.local/share/stemdeck /// Can be overridden by STEMDECK_DATA_DIR for development. fn local_data_dir() -> Result { if let Ok(path) = env::var("STEMDECK_DATA_DIR") { @@ -516,14 +759,24 @@ fn local_data_dir() -> Result { .map_err(|_| "LOCALAPPDATA environment variable not set".to_string())?; Ok(PathBuf::from(base).join("StemDeck")) } - #[cfg(not(windows))] + #[cfg(target_os = "macos")] + { + let home = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; + Ok(PathBuf::from(home) + .join("Library") + .join("Application Support") + .join("StemDeck")) + } + #[cfg(all(unix, not(target_os = "macos")))] { if let Ok(xdg) = env::var("XDG_DATA_HOME") { return Ok(PathBuf::from(xdg).join("stemdeck")); } - let home = env::var("HOME") - .map_err(|_| "HOME environment variable not set".to_string())?; - Ok(PathBuf::from(home).join(".local").join("share").join("stemdeck")) + let home = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?; + Ok(PathBuf::from(home) + .join(".local") + .join("share") + .join("stemdeck")) } } @@ -552,6 +805,257 @@ fn migrate_legacy_data(root: &Path, data_dir: &Path) { } } +fn runtime_dir(data_dir: &Path) -> PathBuf { + data_dir.join("runtime") +} + +fn runtime_python_path(data_dir: &Path) -> PathBuf { + runtime_dir(data_dir) + .join("python") + .join("bin") + .join("python") +} + +fn runtime_manifest_path(root: &Path) -> Option { + [ + root.join("runtime-manifest.json"), + root.join("desktop") + .join("ui") + .join("runtime-manifest.json"), + ] + .into_iter() + .find(|path| path.is_file()) +} + +fn load_runtime_manifest(root: &Path) -> Result { + let path = runtime_manifest_path(root) + .ok_or_else(|| format!("runtime-manifest.json not found under {}", root.display()))?; + read_runtime_manifest(&path) +} + +fn read_runtime_manifest(path: &Path) -> Result { + let text = + fs::read_to_string(path).map_err(|e| format!("failed to read {}: {e}", path.display()))?; + serde_json::from_str(&text).map_err(|e| format!("failed to parse {}: {e}", path.display())) +} + +fn read_runtime_install_manifest(runtime_dir: &Path) -> Option { + let text = fs::read_to_string(runtime_dir.join("runtime-manifest.json")).ok()?; + serde_json::from_str(&text).ok() +} + +fn validate_runtime_manifest(manifest: &RuntimeManifest) -> Result<(), String> { + if manifest.runtime_url.trim().is_empty() { + return Err("runtime manifest has an empty runtimeUrl".to_string()); + } + let sha = manifest.runtime_sha256.trim(); + if sha.len() != 64 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) { + return Err("runtime manifest must include a 64-character runtimeSha256".to_string()); + } + Ok(()) +} + +fn runtime_archive_path(data_dir: &Path, manifest: &RuntimeManifest) -> PathBuf { + let name = manifest + .archive_name + .clone() + .or_else(|| manifest.runtime_url.rsplit('/').next().map(str::to_string)) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| format!("StemDeck-runtime-macOS-{}.tar.zst", manifest.arch)); + data_dir.join("downloads").join(name) +} + +async fn download_file_with_progress( + url: &str, + target: &Path, + app_handle: &tauri::AppHandle, +) -> Result<(), String> { + let tmp = target.with_extension("download"); + if tmp.exists() { + fs::remove_file(&tmp) + .map_err(|e| format!("failed to remove {}: {e}", tmp.display()))?; + } + + if let Some(path) = url.strip_prefix("file://") { + fs::copy(Path::new(path), &tmp) + .map_err(|e| format!("failed to copy runtime pack from {url}: {e}"))?; + return fs::rename(&tmp, target) + .map_err(|e| format!("failed to move runtime pack to {}: {e}", target.display())); + } + if Path::new(url).is_file() { + fs::copy(Path::new(url), &tmp) + .map_err(|e| format!("failed to copy runtime pack from {url}: {e}"))?; + return fs::rename(&tmp, target) + .map_err(|e| format!("failed to move runtime pack to {}: {e}", target.display())); + } + + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(30 * 60)) + .build() + .map_err(|e| format!("failed to build HTTP client: {e}"))?; + let mut response = client + .get(url) + .send() + .await + .map_err(|e| format!("failed to start download from {url}: {e}"))?; + if !response.status().is_success() { + return Err(format!( + "failed to download runtime pack from {url}: HTTP {}", + response.status() + )); + } + + let total = response.content_length(); + let mut file = + fs::File::create(&tmp).map_err(|e| format!("failed to create {}: {e}", tmp.display()))?; + let mut received: u64 = 0; + let mut last_emit = Instant::now(); + + while let Some(chunk) = response + .chunk() + .await + .map_err(|e| format!("download error: {e}"))? + { + file.write_all(&chunk) + .map_err(|e| format!("failed to write to {}: {e}", tmp.display()))?; + received += chunk.len() as u64; + if last_emit.elapsed() >= Duration::from_millis(150) { + let _ = app_handle.emit("runtime-download-progress", DownloadProgress { received, total }); + last_emit = Instant::now(); + } + } + let _ = app_handle.emit( + "runtime-download-progress", + DownloadProgress { received, total: Some(received) }, + ); + + fs::rename(&tmp, target) + .map_err(|e| format!("failed to move runtime pack to {}: {e}", target.display())) +} + +fn download_file(url: &str, target: &Path, timeout: Duration) -> Result<(), String> { + let tmp = target.with_extension("download"); + if tmp.exists() { + fs::remove_file(&tmp).map_err(|e| format!("failed to remove {}: {e}", tmp.display()))?; + } + + if let Some(path) = url.strip_prefix("file://") { + fs::copy(Path::new(path), &tmp) + .map_err(|e| format!("failed to copy runtime pack from {url}: {e}"))?; + } else if Path::new(url).is_file() { + fs::copy(Path::new(url), &tmp) + .map_err(|e| format!("failed to copy runtime pack from {url}: {e}"))?; + } else { + let mut command = Command::new("curl"); + command + .args([ + "--fail", + "--location", + "--show-error", + "--output", + &tmp.display().to_string(), + "--", + url, + ]) + .stdout(Stdio::null()) + .stderr(Stdio::piped()); + let output = command_output_with_timeout(command, timeout, "runtime pack download")?; + if !output.status.success() || !tmp.is_file() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!( + "failed to download runtime pack from {url}: {}", + stderr.trim() + )); + } + } + + fs::rename(&tmp, target) + .map_err(|e| format!("failed to move runtime pack to {}: {e}", target.display())) +} + +fn verify_runtime_archive( + manifest: &RuntimeManifest, + archive: &Path, +) -> Result { + if !archive.is_file() { + return Err(format!( + "runtime archive not found at {}", + archive.display() + )); + } + let size = archive + .metadata() + .map_err(|e| format!("failed to stat {}: {e}", archive.display()))? + .len(); + if let Some(expected) = manifest.runtime_size { + if expected > 0 && expected != size { + return Err(format!( + "runtime archive size mismatch: expected {expected}, got {size}" + )); + } + } + let sha256 = sha256_file(archive)?; + if !sha256.eq_ignore_ascii_case(manifest.runtime_sha256.trim()) { + return Err(format!( + "runtime archive checksum mismatch: expected {}, got {}", + manifest.runtime_sha256, sha256 + )); + } + Ok(RuntimeArchive { + archive_path: archive.display().to_string(), + sha256, + size, + }) +} + +fn sha256_file(path: &Path) -> Result { + let mut file = + fs::File::open(path).map_err(|e| format!("failed to open {}: {e}", path.display()))?; + let mut hasher = Sha256::new(); + let mut buffer = [0_u8; 1024 * 64]; + loop { + let read = file + .read(&mut buffer) + .map_err(|e| format!("failed to read {}: {e}", path.display()))?; + if read == 0 { + break; + } + hasher.update(&buffer[..read]); + } + Ok(format!("{:x}", hasher.finalize())) +} + +fn extract_tar_archive(archive: &Path, destination: &Path) -> Result<(), String> { + let mut command = Command::new("tar"); + command + .args([ + "-xf", + &archive.display().to_string(), + "-C", + &destination.display().to_string(), + ]) + .stdout(Stdio::null()) + .stderr(Stdio::piped()); + let output = command_output_with_timeout( + command, + Duration::from_secs(20 * 60), + "runtime pack extraction", + )?; + if output.status.success() { + Ok(()) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!("failed to extract runtime pack: {}", stderr.trim())) + } +} + +fn unix_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or_default() +} + fn app_root() -> Result { if let Ok(root) = env::var("STEMDECK_ROOT") { return Ok(PathBuf::from(root)); @@ -568,6 +1072,15 @@ fn app_root() -> Result { if let Some(root) = find_repo_root(exe_dir) { return Ok(root); } + #[cfg(target_os = "macos")] + { + if let Some(contents) = exe_dir.parent() { + let resources = contents.join("Resources"); + if resources.is_dir() { + return Ok(resources); + } + } + } Ok(exe_dir.to_path_buf()) } @@ -584,6 +1097,13 @@ fn find_repo_root(start: &Path) -> Option { } fn backend_dir(root: &Path) -> Result { + if let Ok(data_dir) = local_data_dir() { + let backend = runtime_dir(&data_dir).join("backend"); + if backend.join("app").is_dir() { + return Ok(backend); + } + } + let portable = root.join("backend"); if portable.join("app").is_dir() { return Ok(portable); @@ -601,6 +1121,12 @@ fn python_path(root: &Path) -> Option { if let Ok(path) = env::var("STEMDECK_PYTHON") { return Some(PathBuf::from(path)); } + if let Ok(data_dir) = local_data_dir() { + let python = runtime_python_path(&data_dir); + if python.is_file() { + return Some(python); + } + } let candidates = if cfg!(windows) { vec![ root.join("python").join("Scripts").join("python.exe"), @@ -733,13 +1259,110 @@ fn ensure_ffmpeg(data_dir: &Path) -> Result { return Ok(portable); } - #[cfg(not(windows))] + #[cfg(target_os = "macos")] + { + download_macos_ffmpeg(data_dir)?; + let portable = + ffmpeg_path(data_dir).ok_or_else(|| "failed to resolve FFmpeg path".to_string())?; + verify_ffmpeg(&portable)?; + Ok(portable) + } + + #[cfg(all(unix, not(target_os = "macos")))] { verify_ffmpeg(Path::new("ffmpeg"))?; Ok(PathBuf::from("ffmpeg")) } } +#[cfg(target_os = "macos")] +fn download_macos_ffmpeg(data_dir: &Path) -> Result<(), String> { + let ffmpeg_url = env::var("STEMDECK_FFMPEG_URL") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_MACOS_FFMPEG_URL.to_string()); + let ffprobe_url = env::var("STEMDECK_FFPROBE_URL") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_MACOS_FFPROBE_URL.to_string()); + let downloads = data_dir.join("downloads"); + let ffmpeg_zip = downloads.join("ffmpeg-macos.zip"); + let ffprobe_zip = downloads.join("ffprobe-macos.zip"); + fs::create_dir_all(&downloads) + .map_err(|e| format!("failed to create {}: {e}", downloads.display()))?; + + download_file(&ffmpeg_url, &ffmpeg_zip, Duration::from_secs(5 * 60))?; + download_file(&ffprobe_url, &ffprobe_zip, Duration::from_secs(5 * 60))?; + + let ffmpeg_dir = data_dir.join("ffmpeg"); + fs::create_dir_all(&ffmpeg_dir) + .map_err(|e| format!("failed to create {}: {e}", ffmpeg_dir.display()))?; + extract_single_binary_from_zip(&ffmpeg_zip, &ffmpeg_dir.join("ffmpeg"), "ffmpeg")?; + extract_single_binary_from_zip(&ffprobe_zip, &ffmpeg_dir.join("ffprobe"), "ffprobe")?; + + make_executable(&ffmpeg_dir.join("ffmpeg"))?; + make_executable(&ffmpeg_dir.join("ffprobe"))?; + Ok(()) +} + +#[cfg(target_os = "macos")] +fn extract_single_binary_from_zip( + archive_path: &Path, + target: &Path, + binary_name: &str, +) -> Result<(), String> { + let file = fs::File::open(archive_path) + .map_err(|e| format!("failed to open {}: {e}", archive_path.display()))?; + let mut archive = zip::ZipArchive::new(file) + .map_err(|e| format!("failed to read zip {}: {e}", archive_path.display()))?; + + for i in 0..archive.len() { + let mut entry = archive + .by_index(i) + .map_err(|e| format!("failed to read zip entry {i}: {e}"))?; + if !entry.is_file() { + continue; + } + let Some(name) = entry.enclosed_name() else { + continue; + }; + let Some(file_name) = name.file_name().and_then(|value| value.to_str()) else { + continue; + }; + if file_name != binary_name { + continue; + } + let mut output = fs::File::create(target) + .map_err(|e| format!("failed to create {}: {e}", target.display()))?; + std::io::copy(&mut entry, &mut output) + .map_err(|e| format!("failed to extract {}: {e}", target.display()))?; + return Ok(()); + } + + Err(format!( + "downloaded archive {} did not contain {binary_name}", + archive_path.display() + )) +} + +#[cfg(target_os = "macos")] +fn make_executable(path: &Path) -> Result<(), String> { + let output = Command::new("chmod") + .args(["+x", &path.display().to_string()]) + .stderr(Stdio::piped()) + .output() + .map_err(|e| format!("failed to chmod {}: {e}", path.display()))?; + if output.status.success() { + Ok(()) + } else { + Err(format!( + "failed to chmod {}: {}", + path.display(), + String::from_utf8_lossy(&output.stderr).trim() + )) + } +} + #[cfg(windows)] fn download_windows_ffmpeg(data_dir: &Path) -> Result<(), String> { let url = env::var("STEMDECK_FFMPEG_URL") @@ -792,7 +1415,7 @@ fn download_file_with_powershell(url: &str, target: &Path) -> Result<(), String> #[cfg(windows)] fn extract_ffmpeg_binaries(archive_path: &Path, data_dir: &Path) -> Result<(), String> { - let file = File::open(archive_path) + let file = fs::File::open(archive_path) .map_err(|e| format!("failed to open {}: {e}", archive_path.display()))?; let mut archive = ZipArchive::new(file) .map_err(|e| format!("failed to read FFmpeg zip {}: {e}", archive_path.display()))?; @@ -827,7 +1450,7 @@ fn extract_ffmpeg_binaries(archive_path: &Path, data_dir: &Path) -> Result<(), S _ => continue, }; let target = ffmpeg_dir.join(target_name); - let mut output = File::create(&target) + let mut output = fs::File::create(&target) .map_err(|e| format!("failed to create {}: {e}", target.display()))?; std::io::copy(&mut entry, &mut output) .map_err(|e| format!("failed to extract {}: {e}", target.display()))?; @@ -885,8 +1508,15 @@ fn write_setup_config(data_dir: &Path, ffmpeg: &Path) -> Result<(), String> { ), ( "ffmpegSource", - serde_json::json!(env::var("STEMDECK_FFMPEG_URL") - .unwrap_or_else(|_| DEFAULT_WINDOWS_FFMPEG_URL.to_string())), + serde_json::json!(env::var("STEMDECK_FFMPEG_URL").unwrap_or_else(|_| { + if cfg!(windows) { + DEFAULT_WINDOWS_FFMPEG_URL.to_string() + } else if cfg!(target_os = "macos") { + DEFAULT_MACOS_FFMPEG_URL.to_string() + } else { + "system PATH".to_string() + } + })), ), ("updatedAt", serde_json::json!(updated_at)), ], diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 0071b98a..c8c815bd 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "StemDeck", - "version": "0.1.0-alpha.0", + "version": "0.4.0-alpha.1", "identifier": "app.stemdeck.desktop", "build": { "frontendDist": "../ui", @@ -13,12 +13,12 @@ "windows": [ { "title": "StemDeck", - "width": 1440, - "height": 900, - "minWidth": 1440, - "minHeight": 900, + "width": 1280, + "height": 820, + "minWidth": 1024, + "minHeight": 720, "backgroundColor": "#050b10", - "dragDropEnabled": false + "dragDropEnabled": false } ], "security": { @@ -28,6 +28,23 @@ "bundle": { "active": true, "targets": ["app"], - "icon": [] + "icon": ["icons/icon.icns", "icons/icon.png", "icons/icon.ico"], + "macOS": { + "minimumSystemVersion": "13.0", + "dmg": { + "windowSize": { + "width": 660, + "height": 400 + }, + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + } + } + } } } diff --git a/desktop/ui/index.html b/desktop/ui/index.html index 4059f6da..59574c0b 100644 --- a/desktop/ui/index.html +++ b/desktop/ui/index.html @@ -16,7 +16,7 @@

StemDeck

Preparing the local audio engine, first run will take a while...

    -
  1. Checking portable runtime
  2. +
  3. Setting up local runtime
  4. Creating workspace
  5. Checking FFmpeg
  6. Configuring compute device
  7. @@ -25,6 +25,9 @@

    StemDeck

Starting setup...

+
diff --git a/desktop/ui/runtime-manifest.json b/desktop/ui/runtime-manifest.json new file mode 100644 index 00000000..b0f27c3e --- /dev/null +++ b/desktop/ui/runtime-manifest.json @@ -0,0 +1,8 @@ +{ + "version": "0.1.0-alpha.1", + "arch": "arm64", + "runtimeUrl": "", + "runtimeSha256": "", + "runtimeSize": 0, + "archiveName": "StemDeck-runtime-macOS-arm64.tar.zst" +} diff --git a/desktop/ui/setup.css b/desktop/ui/setup.css index 0b1cd3bd..cc3cc7f1 100644 --- a/desktop/ui/setup.css +++ b/desktop/ui/setup.css @@ -151,6 +151,32 @@ h1 { color: #cbd2da; } +.progress-wrap { + height: 4px; + background: rgba(148, 163, 184, 0.15); + border-radius: 2px; + margin-top: 10px; + overflow: hidden; +} + +.progress-fill { + height: 100%; + background: var(--gold); + border-radius: 2px; + width: 0%; + transition: width 0.15s linear; +} + +.progress-fill.indeterminate { + width: 35%; + animation: progress-slide 1.6s ease-in-out infinite; +} + +@keyframes progress-slide { + 0% { transform: translateX(-200%); } + 100% { transform: translateX(400%); } +} + .details { max-height: 180px; overflow: auto; diff --git a/desktop/ui/setup.js b/desktop/ui/setup.js index e7815b6f..0bc2ba39 100644 --- a/desktop/ui/setup.js +++ b/desktop/ui/setup.js @@ -75,6 +75,59 @@ function startProgressStatus(messages) { return () => window.clearInterval(timer); } +function isMac() { + return /mac/i.test(navigator.userAgentData?.platform ?? navigator.platform ?? ""); +} + +async function installRuntimePack(appRoot) { + const status = await invoke("runtime_pack_status"); + if (!status.manifestReady) { + throw new Error( + `Python runtime not found. Expected python/ or .venv/ under ${appRoot}, or a runtime-manifest.json for first-run setup.` + ); + } + + const progressWrap = document.getElementById("progress-wrap"); + const progressFill = document.getElementById("progress-fill"); + + const unlisten = await window.__TAURI__.event.listen( + "runtime-download-progress", + (event) => { + const { received, total } = event.payload; + const mb = (received / 1e6).toFixed(0); + if (total && total > 0) { + const pct = Math.min(100, Math.round((received / total) * 100)); + progressFill.style.width = `${pct}%`; + progressFill.classList.remove("indeterminate"); + setStatus(`Downloading StemDeck runtime... ${mb} / ${(total / 1e6).toFixed(0)} MB`); + } else { + progressFill.classList.add("indeterminate"); + setStatus(`Downloading StemDeck runtime... ${mb} MB received`); + } + } + ); + + progressWrap.classList.remove("hidden"); + progressFill.style.width = "0%"; + progressFill.classList.remove("indeterminate"); + + try { + setStatus("Downloading StemDeck runtime..."); + await invoke("download_runtime_pack"); + progressWrap.classList.add("hidden"); + setStatus("Verifying StemDeck runtime..."); + await invoke("verify_runtime_pack"); + setStatus("Installing StemDeck runtime..."); + const installed = await invoke("extract_runtime_pack"); + if (!installed.runtimeReady) { + throw new Error("Runtime install finished but Python/backend files were not found."); + } + } finally { + unlisten(); + progressWrap.classList.add("hidden"); + } +} + async function runSetup() { detailsEl.classList.add("hidden"); retryBtn.classList.add("hidden"); @@ -83,7 +136,7 @@ async function runSetup() { try { setStep("runtime", "active"); setStatus("Checking Python runtime..."); - const [runtime] = await Promise.all([ + let [runtime] = await Promise.all([ invoke("probe_runtime"), minDelay(350), ]); @@ -107,10 +160,13 @@ async function runSetup() { } if (!runtime.pythonReady) { - setStep("runtime", "error"); - throw new Error( - `Python runtime not found. Expected python/ or .venv/ under: ${runtime.appRoot}` - ); + await invoke("ensure_workspace"); + await installRuntimePack(runtime.appRoot); + runtime = await invoke("probe_runtime"); + if (!runtime.pythonReady) { + setStep("runtime", "error"); + throw new Error(`Python runtime setup failed under: ${runtime.dataDir}`); + } } setStep("runtime", "done"); setStatus(`Python runtime found at ${runtime.pythonPath}`); @@ -149,32 +205,53 @@ async function runSetup() { } await runStep("gpu", async () => { - const stopProgress = startProgressStatus([ - { - afterSeconds: 0, - text: "Checking NVIDIA GPU and compute support...", - }, - { - afterSeconds: 20, - text: "Installing NVIDIA acceleration if needed... first run can take 5-15 minutes.", - }, - { - afterSeconds: 120, - text: "Still installing NVIDIA acceleration... CUDA Torch packages are large.", - }, - { - afterSeconds: 300, - text: "Still working... setup should finish or time out automatically.", - }, - ]); + const macGPU = isMac(); + const stopProgress = startProgressStatus( + macGPU + ? [ + { + afterSeconds: 0, + text: "Checking Apple Silicon compute support...", + }, + { + afterSeconds: 10, + text: "Verifying MPS acceleration for AI models...", + }, + ] + : [ + { + afterSeconds: 0, + text: "Checking NVIDIA GPU and compute support...", + }, + { + afterSeconds: 20, + text: "Installing NVIDIA acceleration if needed... first run can take 5-15 minutes.", + }, + { + afterSeconds: 120, + text: "Still installing NVIDIA acceleration... CUDA Torch packages are large.", + }, + { + afterSeconds: 300, + text: "Still working... setup should finish or time out automatically.", + }, + ] + ); try { const gpu = await invoke("ensure_torch_device"); - gpuSummary = gpu.gpuDetected - ? gpu.cudaVerified - ? `${gpu.gpuName} - CUDA ${gpu.cudaVersion} enabled` - : `${gpu.gpuName} found - falling back to CPU (CUDA unverified)` - : "No NVIDIA GPU - stem separation will use CPU"; + if (macGPU) { + gpuSummary = + gpu.torchDevice === "mps" + ? `${gpu.gpuName} acceleration enabled` + : "MPS acceleration unavailable - stem separation will use CPU"; + } else { + gpuSummary = gpu.gpuDetected + ? gpu.cudaVerified + ? `${gpu.gpuName} - CUDA ${gpu.cudaVersion} enabled` + : `${gpu.gpuName} found - falling back to CPU (CUDA unverified)` + : "No NVIDIA GPU - stem separation will use CPU"; + } return gpu; } finally { stopProgress(); diff --git a/stemdeck.png b/imgs/screenshot/stemdeck.png similarity index 100% rename from stemdeck.png rename to imgs/screenshot/stemdeck.png diff --git a/imgs/stemdeck-svg-assets/stemdeck-icon.svg b/imgs/stemdeck-svg-assets/stemdeck-icon.svg new file mode 100644 index 00000000..0ebce18d --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-icon.svg @@ -0,0 +1,28 @@ + + StemDeck App Icon + Dark rounded square app icon with a golden segmented waveform. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/imgs/stemdeck-svg-assets/stemdeck-logo-horizontal.svg b/imgs/stemdeck-svg-assets/stemdeck-logo-horizontal.svg new file mode 100644 index 00000000..b999facb --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-logo-horizontal.svg @@ -0,0 +1,25 @@ + + StemDeck Horizontal Logo + StemDeck logo with golden waveform symbol and wordmark. + + + + + + + + + + + + + + + + + + + Stem + Deck + POWERED STEM SEPARATION + diff --git a/imgs/stemdeck-svg-assets/stemdeck-logo-stacked.svg b/imgs/stemdeck-svg-assets/stemdeck-logo-stacked.svg new file mode 100644 index 00000000..010c71f7 --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-logo-stacked.svg @@ -0,0 +1,24 @@ + + StemDeck Stacked Logo + Stacked StemDeck logo with waveform symbol above the wordmark. + + + + + + + + + + + + + + + + + + + Stem Deck + POWERED STEM SEPARATION + diff --git a/imgs/stemdeck-svg-assets/stemdeck-tray-light.svg b/imgs/stemdeck-svg-assets/stemdeck-tray-light.svg new file mode 100644 index 00000000..7408017c --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-tray-light.svg @@ -0,0 +1,12 @@ + + StemDeck Tray Icon Light + + + + + + + + + + diff --git a/imgs/stemdeck-svg-assets/stemdeck-tray-monochrome.svg b/imgs/stemdeck-svg-assets/stemdeck-tray-monochrome.svg new file mode 100644 index 00000000..bf2c320b --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-tray-monochrome.svg @@ -0,0 +1,12 @@ + + StemDeck Tray Icon Monochrome + + + + + + + + + + diff --git a/imgs/stemdeck-svg-assets/stemdeck-waveform-symbol.svg b/imgs/stemdeck-svg-assets/stemdeck-waveform-symbol.svg new file mode 100644 index 00000000..cc3dc110 --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-waveform-symbol.svg @@ -0,0 +1,22 @@ + + StemDeck Waveform Symbol + Golden segmented waveform symbol for StemDeck. + + + + + + + + + + + + + + + + + + + diff --git a/imgs/stemdeck-svg-assets/stemdeck-wordmark.svg b/imgs/stemdeck-svg-assets/stemdeck-wordmark.svg new file mode 100644 index 00000000..c7786f74 --- /dev/null +++ b/imgs/stemdeck-svg-assets/stemdeck-wordmark.svg @@ -0,0 +1,6 @@ + + StemDeck Wordmark + Stem + Deck + POWERED STEM SEPARATION + diff --git a/packaging/macos/README-macOS.txt b/packaging/macos/README-macOS.txt new file mode 100644 index 00000000..a5502959 --- /dev/null +++ b/packaging/macos/README-macOS.txt @@ -0,0 +1,31 @@ +StemDeck for macOS +================== + +Install: + +1. Open the StemDeck DMG. +2. Drag StemDeck.app to Applications. +3. Open StemDeck from Applications. + +First launch: + +- StemDeck is a thin native app. It downloads a pinned, checksummed StemDeck + runtime pack on first launch. +- The runtime installs to: + ~/Library/Application Support/StemDeck/runtime +- FFmpeg and ffprobe install to: + ~/Library/Application Support/StemDeck/ffmpeg +- Demucs model weights download on first use and are cached under: + ~/Library/Application Support/StemDeck/models + +Uninstall: + +1. Delete /Applications/StemDeck.app. +2. To remove runtime files, jobs, caches, models, and logs, delete: + ~/Library/Application Support/StemDeck + +Notes: + +- Internet access is required for first-run setup. +- Public releases should be signed and notarized. +- Unsigned local builds are for development and internal testing only. diff --git a/packaging/macos/THIRD_PARTY_NOTICES.txt b/packaging/macos/THIRD_PARTY_NOTICES.txt new file mode 100644 index 00000000..171b32cf --- /dev/null +++ b/packaging/macos/THIRD_PARTY_NOTICES.txt @@ -0,0 +1,20 @@ +THIRD-PARTY NOTICES +=================== + +StemDeck for macOS uses a thin Tauri app plus a downloaded runtime pack. + +Bundled in StemDeck.app: + +- Tauri v2 +- The system WebKit WebView provided by macOS + +Downloaded during first-run setup: + +- StemDeck runtime pack containing Python and Python package dependencies +- FFmpeg and ffprobe +- Demucs model weights + +The runtime pack includes its own dependency inventory under +runtime/licenses/pip-list.json. Before public release, generate and include +full license texts for every packaged dependency and verify the exact FFmpeg +build license/provenance. diff --git a/packaging/macos/dmg-background.svg b/packaging/macos/dmg-background.svg new file mode 100644 index 00000000..5b7cb979 --- /dev/null +++ b/packaging/macos/dmg-background.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Drag StemDeck to Applications + diff --git a/pyproject.toml b/pyproject.toml index 9c63fde0..0992f044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,18 +8,28 @@ dependencies = [ "uvicorn[standard]>=0.30", "yt-dlp>=2024.12.0", "demucs>=4.0.1", - # Pin torch/torchaudio to 2.6.x — torchaudio 2.7+ removed its built-in + # Pin torch/torchaudio to 2.6.x where available — torchaudio 2.7+ removed its built-in # audio writer and now requires the separate `torchcodec` package, which # has ABI issues with torch 2.11. Demucs 4.0.1 calls torchaudio.save() to # write stems, so it breaks on 2.7+. - "torch>=2.6,<2.7", - "torchaudio>=2.6,<2.7", + "torch>=2.6,<2.7; sys_platform != 'darwin' or platform_machine != 'x86_64'", + "torchaudio>=2.6,<2.7; sys_platform != 'darwin' or platform_machine != 'x86_64'", + # PyTorch does not publish macOS x86_64 wheels for 2.6.x. Keep Intel macOS + # on the last compatible line so the x64 runtime pack can still be built. + "torch>=2.2,<2.3; sys_platform == 'darwin' and platform_machine == 'x86_64'", + "torchaudio>=2.2,<2.3; sys_platform == 'darwin' and platform_machine == 'x86_64'", # torchaudio 2.6 dispatches WAV writes through libsndfile via soundfile. # Without this, demucs crashes on save with "Couldn't find appropriate # backend to handle uri ... drums.wav". "soundfile>=0.12", # BPM + key analysis on the downloaded source. Pulls numpy/scipy/numba. "librosa>=0.10", + # Current llvmlite releases no longer publish macOS x86_64 wheels, which + # makes Intel runtime builds require a full LLVM/CMake toolchain. Keep Intel + # macOS on a wheel-backed Numba line. + "numba>=0.61,<0.62; sys_platform == 'darwin' and platform_machine == 'x86_64'", + # Torch 2.2's Intel macOS wheel was built against NumPy 1.x. + "numpy<2; sys_platform == 'darwin' and platform_machine == 'x86_64'", # ITU-R BS.1770 integrated-loudness (LUFS) measurement for the # post-analysis loudness card. Pure Python, depends on scipy which # librosa already pulls in. @@ -27,6 +37,10 @@ dependencies = [ # FastAPI multipart/form-data (file uploads) unconditionally require # python-multipart — it is not an optional extra. "python-multipart>=0.0.9", + # CVE-2026-44431 / CVE-2026-44432: sensitive header forwarding and + # decompression-bomb bypass fixed in 2.7.0. urllib3 is a transitive + # dep via requests; pin floor to pull in the fix. + "urllib3>=2.7.0", ] [project.optional-dependencies] diff --git a/scripts/macos/make-app.sh b/scripts/macos/make-app.sh new file mode 100755 index 00000000..99cdbc22 --- /dev/null +++ b/scripts/macos/make-app.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH="${ARCH:-arm64}" +VERSION="${VERSION:-LOCAL_DEV_TEST}" +VERSION="${VERSION#v}" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="${REPO_ROOT}/.build" + +if [[ "$(uname)" != "Darwin" ]]; then + echo "ERROR: make-app.sh must run on macOS" >&2 + exit 1 +fi + +if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then + echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2 + exit 1 +fi + +for cmd in node npm cargo; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command not found on PATH: $cmd" >&2 + exit 1 + fi +done + +mkdir -p "$BUILD_DIR" + +echo "==> Stamping version ${VERSION}" +sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" "$REPO_ROOT/desktop/src-tauri/Cargo.toml" +sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$REPO_ROOT/desktop/src-tauri/tauri.conf.json" +sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" "$REPO_ROOT/desktop/package.json" + +cd "$REPO_ROOT/desktop" + +npm ci +if [[ "$ARCH" == "arm64" ]]; then + npx tauri build --bundles app --target aarch64-apple-darwin + TARGET_DIR="$REPO_ROOT/desktop/src-tauri/target/aarch64-apple-darwin/release" +else + npx tauri build --bundles app --target x86_64-apple-darwin + TARGET_DIR="$REPO_ROOT/desktop/src-tauri/target/x86_64-apple-darwin/release" +fi + +APP_DIR="${TARGET_DIR}/bundle/macos/StemDeck.app" +if [[ ! -d "$APP_DIR" ]]; then + APP_DIR="$(find "$REPO_ROOT/desktop/src-tauri/target" -path '*/bundle/macos/StemDeck.app' -type d | head -1)" +fi +if [[ -z "$APP_DIR" || ! -d "$APP_DIR" ]]; then + echo "ERROR: could not find built StemDeck.app" >&2 + exit 1 +fi + +RESOURCES="${APP_DIR}/Contents/Resources" +mkdir -p "$RESOURCES" + +if [[ -f "$BUILD_DIR/runtime-manifest.json" ]]; then + cp "$BUILD_DIR/runtime-manifest.json" "$RESOURCES/runtime-manifest.json" +else + cp "$REPO_ROOT/desktop/ui/runtime-manifest.json" "$RESOURCES/runtime-manifest.json" +fi + +if [[ -f "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" ]]; then + cp "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" "$RESOURCES/THIRD_PARTY_NOTICES.txt" +fi + +echo "$APP_DIR" > "$BUILD_DIR/app-path.txt" +echo "==> App ready: $APP_DIR" diff --git a/scripts/macos/make-dmg.sh b/scripts/macos/make-dmg.sh new file mode 100755 index 00000000..a79c396a --- /dev/null +++ b/scripts/macos/make-dmg.sh @@ -0,0 +1,139 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH="${ARCH:-arm64}" +VERSION="${VERSION:-LOCAL_DEV_TEST}" +VERSION="${VERSION#v}" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="${REPO_ROOT}/.build" +DIST_DIR="${BUILD_DIR}/macos-dist" +DMG_STAGING="${BUILD_DIR}/dmg-staging" +DMG_NAME="StemDeck-macOS-${ARCH}.dmg" +DMG_PATH="${DIST_DIR}/${DMG_NAME}" +DMG_RW_PATH="${DIST_DIR}/StemDeck-macOS-${ARCH}.rw.dmg" +RUNTIME_NAME="StemDeck-runtime-macOS-${ARCH}.tar.zst" +RUNTIME_PATH="${BUILD_DIR}/${RUNTIME_NAME}" +BACKGROUND_SRC="${REPO_ROOT}/packaging/macos/dmg-background.svg" +BACKGROUND_DIR_NAME=".background" +BACKGROUND_PNG_NAME="dmg-background.png" + +if [[ "$(uname)" != "Darwin" ]]; then + echo "ERROR: make-dmg.sh must run on macOS" >&2 + exit 1 +fi + +if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then + echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2 + exit 1 +fi + +for cmd in ditto hdiutil qlmanage shasum; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command not found on PATH: $cmd" >&2 + exit 1 + fi +done + +if [[ "$ARCH" == "arm64" ]]; then + APP_DIR="${REPO_ROOT}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/StemDeck.app" +else + APP_DIR="${REPO_ROOT}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle/macos/StemDeck.app" +fi + +if [[ ! -d "$APP_DIR" ]]; then + echo "ERROR: built app not found: $APP_DIR" >&2 + echo "Run: ARCH=${ARCH} scripts/macos/make-app.sh" >&2 + exit 1 +fi + +if [[ ! -f "$RUNTIME_PATH" ]]; then + echo "ERROR: runtime pack not found: $RUNTIME_PATH" >&2 + echo "Run: ARCH=${ARCH} VERSION=${VERSION} scripts/macos/make-runtime-pack.sh" >&2 + exit 1 +fi + +rm -rf "$DMG_STAGING" +mkdir -p "$DMG_STAGING" "$DIST_DIR" +mkdir -p "$DMG_STAGING/$BACKGROUND_DIR_NAME" + +ditto "$APP_DIR" "$DMG_STAGING/StemDeck.app" +ln -s /Applications "$DMG_STAGING/Applications" + +if [[ -f "$BACKGROUND_SRC" ]]; then + qlmanage -t -s 1320 -o "$DMG_STAGING/$BACKGROUND_DIR_NAME" "$BACKGROUND_SRC" >/dev/null 2>&1 + mv "$DMG_STAGING/$BACKGROUND_DIR_NAME/dmg-background.svg.png" "$DMG_STAGING/$BACKGROUND_DIR_NAME/$BACKGROUND_PNG_NAME" +fi + +if [[ -f "$REPO_ROOT/packaging/macos/README-macOS.txt" ]]; then + cp "$REPO_ROOT/packaging/macos/README-macOS.txt" "$DMG_STAGING/README-macOS.txt" +fi + +if [[ -f "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" ]]; then + cp "$REPO_ROOT/packaging/macos/THIRD_PARTY_NOTICES.txt" "$DMG_STAGING/THIRD_PARTY_NOTICES.txt" +fi + +rm -f "$DMG_PATH" "$DMG_RW_PATH" +hdiutil create \ + -volname "StemDeck" \ + -srcfolder "$DMG_STAGING" \ + -ov \ + -format UDRW \ + "$DMG_RW_PATH" + +MOUNT_DIR="$(mktemp -d /tmp/stemdeck-dmg.XXXXXX)" +cleanup_mount() { + hdiutil detach "$MOUNT_DIR" >/dev/null 2>&1 || true + rmdir "$MOUNT_DIR" >/dev/null 2>&1 || true +} +trap cleanup_mount EXIT + +hdiutil attach "$DMG_RW_PATH" -readwrite -noverify -nobrowse -mountpoint "$MOUNT_DIR" >/dev/null + +if command -v SetFile >/dev/null 2>&1; then + SetFile -a V "$MOUNT_DIR/$BACKGROUND_DIR_NAME" || true + SetFile -a V "$MOUNT_DIR/README-macOS.txt" || true + SetFile -a V "$MOUNT_DIR/THIRD_PARTY_NOTICES.txt" || true +fi + +if [[ -f "$MOUNT_DIR/$BACKGROUND_DIR_NAME/$BACKGROUND_PNG_NAME" ]]; then + osascript </dev/null +rmdir "$MOUNT_DIR" +trap - EXIT + +hdiutil convert "$DMG_RW_PATH" -format UDZO -imagekey zlib-level=9 -o "$DMG_PATH" >/dev/null +rm -f "$DMG_RW_PATH" + +CHECKSUMS_PATH="${DIST_DIR}/SHA256SUMS-macOS-${ARCH}.txt" +{ + shasum -a 256 "$DMG_PATH" + shasum -a 256 "$RUNTIME_PATH" +} | sed "s#${REPO_ROOT}/##" > "$CHECKSUMS_PATH" + +echo "==> DMG ready: $DMG_PATH" +echo "==> Runtime pack: $RUNTIME_PATH" +echo "==> Checksums: $CHECKSUMS_PATH" diff --git a/scripts/macos/make-iconset.sh b/scripts/macos/make-iconset.sh new file mode 100755 index 00000000..a4a84ddc --- /dev/null +++ b/scripts/macos/make-iconset.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +SOURCE_SVG="${SOURCE_SVG:-${REPO_ROOT}/imgs/stemdeck-svg-assets/stemdeck-icon.svg}" +ICON_DIR="${REPO_ROOT}/desktop/src-tauri/icons" +WORK_DIR="${TMPDIR:-/tmp}/stemdeck-iconset" +RENDER_DIR="${WORK_DIR}/render" +ICONSET_DIR="${WORK_DIR}/icon.iconset" + +if [[ "$(uname)" != "Darwin" ]]; then + echo "ERROR: make-iconset.sh must run on macOS" >&2 + exit 1 +fi + +for cmd in qlmanage sips iconutil; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command not found on PATH: $cmd" >&2 + exit 1 + fi +done + +if [[ ! -f "$SOURCE_SVG" ]]; then + echo "ERROR: source SVG not found: $SOURCE_SVG" >&2 + exit 1 +fi + +rm -rf "$WORK_DIR" +mkdir -p "$RENDER_DIR" "$ICONSET_DIR" "$ICON_DIR" + +qlmanage -t -s 1024 -o "$RENDER_DIR" "$SOURCE_SVG" >/dev/null +RENDERED_PNG="$(find "$RENDER_DIR" -maxdepth 1 -type f -name '*.png' | head -1)" +if [[ -z "$RENDERED_PNG" || ! -f "$RENDERED_PNG" ]]; then + echo "ERROR: qlmanage did not render a PNG from $SOURCE_SVG" >&2 + exit 1 +fi + +cp "$RENDERED_PNG" "$ICON_DIR/icon.png" + +for size in 16 32 128 256 512; do + sips -z "$size" "$size" "$ICON_DIR/icon.png" \ + --out "$ICONSET_DIR/icon_${size}x${size}.png" >/dev/null +done + +sips -z 32 32 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_16x16@2x.png" >/dev/null +sips -z 64 64 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_32x32@2x.png" >/dev/null +sips -z 256 256 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_128x128@2x.png" >/dev/null +sips -z 512 512 "$ICON_DIR/icon.png" --out "$ICONSET_DIR/icon_256x256@2x.png" >/dev/null +cp "$ICON_DIR/icon.png" "$ICONSET_DIR/icon_512x512@2x.png" + +iconutil -c icns "$ICONSET_DIR" -o "$ICON_DIR/icon.icns" + +echo "==> Icon PNG: $ICON_DIR/icon.png" +echo "==> Icon ICNS: $ICON_DIR/icon.icns" diff --git a/scripts/macos/make-runtime-pack.sh b/scripts/macos/make-runtime-pack.sh new file mode 100755 index 00000000..94c449c9 --- /dev/null +++ b/scripts/macos/make-runtime-pack.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH="${ARCH:-arm64}" +VERSION="${VERSION:-LOCAL_DEV_TEST}" +VERSION="${VERSION#v}" +RELEASE_BASE_URL="${RELEASE_BASE_URL:-https://github.com/thcp/stemdeck/releases/download/v${VERSION}}" +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +BUILD_DIR="${REPO_ROOT}/.build" +STAGING="${BUILD_DIR}/runtime-staging" +RUNTIME_DIR="${STAGING}/runtime" +PYTHON_DIR="${RUNTIME_DIR}/python" +BACKEND_DIR="${RUNTIME_DIR}/backend" + +if [[ "$(uname)" != "Darwin" ]]; then + echo "ERROR: make-runtime-pack.sh must run on macOS" >&2 + exit 1 +fi + +if [[ "$ARCH" != "arm64" && "$ARCH" != "x64" ]]; then + echo "ERROR: ARCH must be arm64 or x64, got '${ARCH}'" >&2 + exit 1 +fi + +PYTHON_BIN="${PYTHON_BIN:-}" +if [[ -z "$PYTHON_BIN" ]]; then + for candidate in python3.12 python3.11 python3.10 python3; do + if command -v "$candidate" >/dev/null 2>&1; then + PYTHON_BIN="$candidate" + break + fi + done +fi + +for cmd in shasum tar "$PYTHON_BIN"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + echo "ERROR: required command not found on PATH: $cmd" >&2 + exit 1 + fi +done + +copy_python_runtime_libs() { + local source_python="$1" + local venv_dir="$2" + local lib_dir="${venv_dir}/lib" + mkdir -p "$lib_dir" + + "$source_python" - <<'PY' | while IFS= read -r path; do +import os +import sys +import sysconfig + +names = [ + sysconfig.get_config_var("LDLIBRARY"), + f"libpython{sys.version_info.major}.{sys.version_info.minor}.dylib", +] +dirs = [ + sysconfig.get_config_var("LIBDIR"), + os.path.join(sys.base_prefix, "lib"), + os.path.join(os.path.dirname(sys.executable), "..", "lib"), +] +seen = set() +for directory in dirs: + if not directory: + continue + directory = os.path.abspath(directory) + for name in names: + if not name: + continue + path = os.path.join(directory, name) + if os.path.isfile(path) and path not in seen: + seen.add(path) + print(path) +PY + cp -f "$path" "$lib_dir/" + done +} + +PYTHON_VERSION="$("$PYTHON_BIN" - <<'PY' +import sys +print(f"{sys.version_info.major}.{sys.version_info.minor}") +PY +)" +PYTHON_MACHINE="$("$PYTHON_BIN" - <<'PY' +import platform +print(platform.machine()) +PY +)" +case "$PYTHON_VERSION" in + 3.10|3.11|3.12|3.13) ;; + *) + echo "ERROR: ${PYTHON_BIN} is Python ${PYTHON_VERSION}; Torch 2.6 runtime builds require Python 3.10-3.13." >&2 + echo "Set PYTHON_BIN=/path/to/python3.12 or PYTHON_BIN=/path/to/python3.11." >&2 + exit 1 + ;; +esac + +HOST_ARCH="$(uname -m)" +if [[ "$ARCH" == "arm64" && "$PYTHON_MACHINE" != "arm64" ]]; then + echo "ERROR: arm64 runtime requires an arm64 Python, got ${PYTHON_MACHINE}" >&2 + exit 1 +fi +if [[ "$ARCH" == "x64" && "$PYTHON_MACHINE" != "x86_64" ]]; then + echo "ERROR: x64 runtime requires an x86_64 Python, got ${PYTHON_MACHINE}" >&2 + exit 1 +fi +if [[ "$ARCH" == "x64" && "$HOST_ARCH" == "arm64" ]]; then + if ! arch -x86_64 /usr/bin/true >/dev/null 2>&1; then + echo "ERROR: x64 runtime on arm64 hosts requires Rosetta 2." >&2 + exit 1 + fi +fi + +rm -rf "$STAGING" +mkdir -p "$PYTHON_DIR" "$BACKEND_DIR" "$BUILD_DIR" + +echo "==> Creating Python runtime (${ARCH})" +echo "==> Python: $("$PYTHON_BIN" --version)" +echo "==> Python architecture: ${PYTHON_MACHINE}" +"$PYTHON_BIN" -m venv --copies --without-pip "$PYTHON_DIR" +copy_python_runtime_libs "$PYTHON_BIN" "$PYTHON_DIR" +"$PYTHON_DIR/bin/python" -m ensurepip --upgrade --default-pip +"$PYTHON_DIR/bin/python" -m pip install --upgrade pip setuptools wheel +"$PYTHON_DIR/bin/python" -m pip install "$REPO_ROOT" + +echo "==> Verifying runtime imports" +"$PYTHON_DIR/bin/python" - <<'PY' +import importlib +packages = [ + "fastapi", "uvicorn", "yt_dlp", "demucs", "torch", "torchaudio", + "librosa", "pyloudnorm", "soundfile", +] +for package in packages: + importlib.import_module(package) + print(f" OK {package}") +PY + +echo "==> Staging backend" +cp -R "$REPO_ROOT/app" "$BACKEND_DIR/app" +cp -R "$REPO_ROOT/static" "$BACKEND_DIR/static" +cp "$REPO_ROOT/pyproject.toml" "$BACKEND_DIR/pyproject.toml" +cp "$REPO_ROOT/uv.lock" "$BACKEND_DIR/uv.lock" + +cat > "$BACKEND_DIR/static/version.json" < Capturing dependency inventory" +mkdir -p "$RUNTIME_DIR/licenses" +"$PYTHON_DIR/bin/python" -m pip list --format=json > "$RUNTIME_DIR/licenses/pip-list.json" + +cat > "$RUNTIME_DIR/runtime-manifest.json" < Stripping Python caches" +find "$PYTHON_DIR" -type d -name "__pycache__" -prune -exec rm -rf {} + 2>/dev/null || true +find "$PYTHON_DIR" -type f \( -name "*.pyc" -o -name "*.pyo" \) -delete + +ARCHIVE_NAME="StemDeck-runtime-macOS-${ARCH}.tar.zst" +ARCHIVE_PATH="${BUILD_DIR}/${ARCHIVE_NAME}" +if command -v zstd >/dev/null 2>&1; then + tar --zstd -cf "$ARCHIVE_PATH" -C "$STAGING" runtime +else + ARCHIVE_NAME="StemDeck-runtime-macOS-${ARCH}.tar.gz" + ARCHIVE_PATH="${BUILD_DIR}/${ARCHIVE_NAME}" + tar -czf "$ARCHIVE_PATH" -C "$STAGING" runtime +fi + +SIZE="$(stat -f%z "$ARCHIVE_PATH")" +SHA256="$(shasum -a 256 "$ARCHIVE_PATH" | awk '{print $1}')" +RUNTIME_URL="${RELEASE_BASE_URL}/${ARCHIVE_NAME}" + +cat > "${BUILD_DIR}/runtime-manifest.json" < Runtime pack ready" +echo "Archive: ${ARCHIVE_PATH}" +echo "Size: ${SIZE}" +echo "SHA256: ${SHA256}" +echo "Manifest: ${BUILD_DIR}/runtime-manifest.json" diff --git a/static/css/appbar.css b/static/css/appbar.css index 78f48c67..52da6073 100644 --- a/static/css/appbar.css +++ b/static/css/appbar.css @@ -207,6 +207,12 @@ letter-spacing: -0.01em; } +.brand-logo { + display: block; + width: 190px; + height: auto; +} + .beta-badge { display: inline-flex; align-items: center; diff --git a/static/css/responsive.css b/static/css/responsive.css index f24240e7..1dfbb04d 100644 --- a/static/css/responsive.css +++ b/static/css/responsive.css @@ -128,6 +128,9 @@ .brand { font-size: 22px; } + .brand-logo { + width: 146px; + } .brand-sub { font-size: 12px; } diff --git a/static/css/widgets.css b/static/css/widgets.css index 483cdcb4..38b604a8 100644 --- a/static/css/widgets.css +++ b/static/css/widgets.css @@ -105,7 +105,7 @@ .topbar .appbar-body-inner { display: grid !important; - grid-template-columns: 210px minmax(0, 1fr); + grid-template-columns: 270px minmax(0, 1fr); align-items: center; gap: 18px; width: 100%; @@ -136,7 +136,7 @@ } .topbar .brand-row { - gap: 9px; + gap: 14px; } .topbar .brand { @@ -144,6 +144,10 @@ letter-spacing: 0; } +.topbar .brand-logo { + width: 190px; +} + .topbar .beta-badge { height: 20px; border-radius: 5px; @@ -156,14 +160,6 @@ display: none !important; } -.topbar .brand-version.has-update { - display: block !important; - margin-top: 5px; - font-size: 11.5px; - line-height: 1.15; - font-weight: 600; -} - .topbar .brand-version.has-update a { display: inline-flex; align-items: center; @@ -275,7 +271,7 @@ @media (max-width: 1250px) { .topbar .appbar-body-inner { - grid-template-columns: 178px minmax(0, 1fr); + grid-template-columns: 236px minmax(0, 1fr); gap: 14px; } @@ -283,6 +279,10 @@ font-size: 20px; } + .topbar .brand-logo { + width: 170px; + } + .topbar .beta-badge { height: 18px; padding: 0 5px; diff --git a/static/imgs/stemdeck-logo-horizontal.svg b/static/imgs/stemdeck-logo-horizontal.svg new file mode 100644 index 00000000..b999facb --- /dev/null +++ b/static/imgs/stemdeck-logo-horizontal.svg @@ -0,0 +1,25 @@ + + StemDeck Horizontal Logo + StemDeck logo with golden waveform symbol and wordmark. + + + + + + + + + + + + + + + + + + + Stem + Deck + POWERED STEM SEPARATION + diff --git a/static/index.html b/static/index.html index 00978827..ac3357a5 100644 --- a/static/index.html +++ b/static/index.html @@ -31,7 +31,9 @@
-

StemDeck

+

+ +

ALPHA
AI stem separation diff --git a/uv.lock b/uv.lock index 1fbc35d2..647a0af7 100644 --- a/uv.lock +++ b/uv.lock @@ -2,10 +2,14 @@ version = 1 revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] [[package]] @@ -370,8 +374,10 @@ dependencies = [ { name = "lameenc" }, { name = "openunmix" }, { name = "pyyaml" }, - { name = "torch" }, - { name = "torchaudio" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "torchaudio", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torchaudio", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/87/38/55f835ebd9f443465087a6954ede19d4a41aebdf5e28567e89b99d6d2f57/demucs-4.0.1.tar.gz", hash = "sha256:e45a5a788bae79767c37bbf6e69aae03862ddcca05550fb79b926346a177d713", size = 1212924, upload-time = "2023-09-07T16:09:01.334Z" } @@ -384,7 +390,8 @@ dependencies = [ { name = "omegaconf" }, { name = "retrying" }, { name = "submitit" }, - { name = "torch" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "treetable" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d5/9d/9a13947db237375486c0690f4741dd2b7e1eee20e0ffcb55dbd1b21cc600/dora_search-0.1.12.tar.gz", hash = "sha256:2956fd2c4c7e4b9a4830e83f0d4cf961be45cfba1a2f0570281e91d15ac516fb", size = 87111, upload-time = "2023-05-23T14:36:24.743Z" } @@ -568,7 +575,8 @@ name = "julius" version = "0.2.7" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "torch" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/19/c9e1596b5572c786b93428d0904280e964c930fae7e6c9368ed9e1b63922/julius-0.2.7.tar.gz", hash = "sha256:3c0f5f5306d7d6016fcc95196b274cae6f07e2c9596eed314e4e7641554fbb08", size = 59640, upload-time = "2022-09-19T16:13:34.2Z" } @@ -643,9 +651,11 @@ dependencies = [ { name = "joblib" }, { name = "lazy-loader" }, { name = "msgpack" }, - { name = "numba" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numba", version = "0.65.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, { name = "pooch" }, { name = "scikit-learn", version = "1.7.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scikit-learn", version = "1.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -662,10 +672,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b5/ba/c63c5786dfee4c3417094c4b00966e61e4a63efecee22cb7b4c0387dda83/librosa-0.11.0-py3-none-any.whl", hash = "sha256:0b6415c4fd68bff4c29288abe67c6d80b587e0e1e2cfb0aad23e4559504a7fa1", size = 260749, upload-time = "2025-03-11T15:09:52.982Z" }, ] +[[package]] +name = "llvmlite" +version = "0.44.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/75/d4863ddfd8ab5f6e70f4504cf8cc37f4e986ec6910f4ef8502bb7d3c1c71/llvmlite-0.44.0-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:9fbadbfba8422123bab5535b293da1cf72f9f478a65645ecd73e781f962ca614", size = 28132306, upload-time = "2025-01-20T11:12:18.634Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, + { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, + { url = "https://files.pythonhosted.org/packages/89/24/4c0ca705a717514c2092b18476e7a12c74d34d875e05e4d742618ebbf449/llvmlite-0.44.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:319bddd44e5f71ae2689859b7203080716448a3cd1128fb144fe5c055219d516", size = 28132306, upload-time = "2025-01-20T11:14:09.035Z" }, +] + [[package]] name = "llvmlite" version = "0.47.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, @@ -854,7 +888,8 @@ name = "networkx" version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ @@ -866,23 +901,54 @@ name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] +[[package]] +name = "numba" +version = "0.61.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "llvmlite", version = "0.44.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a0/e21f57604304aa03ebb8e098429222722ad99176a4f979d34af1d1ee80da/numba-0.61.2.tar.gz", hash = "sha256:8750ee147940a6637b80ecf7f95062185ad8726c8c28a2295b8ec1160a196f7d", size = 2820615, upload-time = "2025-04-09T02:58:07.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/ca/f470be59552ccbf9531d2d383b67ae0b9b524d435fb4a0d229fef135116e/numba-0.61.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:cf9f9fc00d6eca0c23fc840817ce9f439b9f03c8f03d6246c0e7f0cb15b7162a", size = 2775663, upload-time = "2025-04-09T02:57:34.143Z" }, + { url = "https://files.pythonhosted.org/packages/3f/97/c99d1056aed767503c228f7099dc11c402906b42a4757fec2819329abb98/numba-0.61.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:efd3db391df53aaa5cfbee189b6c910a5b471488749fd6606c3f33fc984c2ae2", size = 2775825, upload-time = "2025-04-09T02:57:43.442Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a0/c6b7b9c615cfa3b98c4c63f4316e3f6b3bbe2387740277006551784218cd/numba-0.61.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:34fba9406078bac7ab052efbf0d13939426c753ad72946baaa5bf9ae0ebb8dd2", size = 2776626, upload-time = "2025-04-09T02:57:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f3/0fe4c1b1f2569e8a18ad90c159298d862f96c3964392a20d74fc628aee44/numba-0.61.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:3a10a8fc9afac40b1eac55717cece1b8b1ac0b946f5065c89e00bde646b5b154", size = 2771785, upload-time = "2025-04-09T02:57:59.96Z" }, +] + [[package]] name = "numba" version = "0.65.1" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] dependencies = [ - { name = "llvmlite" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz", hash = "sha256:19357146c32fe9ed25059ab915e8465fb13951cf6b0aace3826b76886373ab23", size = 2765600, upload-time = "2026-04-24T02:02:56.551Z" } wheels = [ @@ -912,12 +978,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/36/8be7118ffd4c8440881046eac3d0982cc5ab42909508cf5d67024d62a2e4/numba-0.65.1-cp314-cp314t-win_amd64.whl", hash = "sha256:20609346e3bd75204950dcbbfe383a8d7dbf4902f442aedbf00f97fef4aa8f38", size = 2758237, upload-time = "2026-04-24T02:02:54.612Z" }, ] +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, +] + [[package]] name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -982,9 +1065,9 @@ name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ @@ -1098,7 +1181,7 @@ name = "nvidia-cudnn-cu12" version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741, upload-time = "2024-04-22T15:24:15.253Z" }, @@ -1109,7 +1192,7 @@ name = "nvidia-cufft-cu12" version = "11.2.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/27/94/3266821f65b92b3138631e9c8e7fe1fb513804ac934485a8d05776e1dd43/nvidia_cufft_cu12-11.2.1.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f083fc24912aa410be21fa16d157fed2055dab1cc4b6934a0e03cba69eb242b9", size = 211459117, upload-time = "2024-04-03T20:57:40.402Z" }, @@ -1128,9 +1211,9 @@ name = "nvidia-cusolver-cu12" version = "11.6.1.9" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/3a/e1/5b9089a4b2a4790dfdea8b3a006052cfecff58139d5a4e34cb1a51df8d6f/nvidia_cusolver_cu12-11.6.1.9-py3-none-manylinux2014_x86_64.whl", hash = "sha256:19e33fa442bcfd085b3086c4ebf7e8debc07cfe01e11513cc6d332fd918ac260", size = 127936057, upload-time = "2024-04-03T20:58:28.735Z" }, @@ -1141,7 +1224,7 @@ name = "nvidia-cusparse-cu12" version = "12.3.1.170" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/db/f7/97a9ea26ed4bbbfc2d470994b8b4f338ef663be97b8f677519ac195e113d/nvidia_cusparse_cu12-12.3.1.170-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ea4f11a2904e2a8dc4b1833cc1b5181cde564edd0d5cd33e3c168eff2d1863f1", size = 207454763, upload-time = "2024-04-03T20:58:59.995Z" }, @@ -1197,10 +1280,13 @@ name = "openunmix" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "torch" }, - { name = "torchaudio" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "torchaudio", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torchaudio", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/ef/4ad54e3ecb1e89f7f7bdb4c7b751e43754e892d3c32a8550e5d0882565df/openunmix-1.3.0.tar.gz", hash = "sha256:cc9245ce728700f5d0b72c67f01be4162777e617cdc47f9b035963afac180fc8", size = 45889, upload-time = "2024-04-16T11:10:47.121Z" } @@ -1403,8 +1489,9 @@ name = "pyloudnorm" version = "0.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] @@ -1581,11 +1668,13 @@ name = "scikit-learn" version = "1.7.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ { name = "joblib", marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "threadpoolctl", marker = "python_full_version < '3.11'" }, ] @@ -1628,13 +1717,17 @@ name = "scikit-learn" version = "1.8.0" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] dependencies = [ { name = "joblib", marker = "python_full_version >= '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "threadpoolctl", marker = "python_full_version >= '3.11'" }, ] @@ -1683,10 +1776,12 @@ name = "scipy" version = "1.15.3" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } wheels = [ @@ -1742,12 +1837,16 @@ name = "scipy" version = "1.17.1" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version >= '3.13'", - "python_full_version == '3.12.*'", - "python_full_version == '3.11.*'", + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", ] dependencies = [ - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } wheels = [ @@ -1828,8 +1927,9 @@ version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/41/9b873a8c055582859b239be17902a85339bec6a30ad162f98c9b0288a2cc/soundfile-0.13.1.tar.gz", hash = "sha256:b2c68dab1e30297317080a5b43df57e302584c49e2942defdde0acccc53f0e5b", size = 46156, upload-time = "2025-01-25T09:17:04.831Z" } wheels = [ @@ -1847,8 +1947,9 @@ name = "soxr" version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/7e/f4b461944662ad75036df65277d6130f9411002bfb79e9df7dff40a31db9/soxr-1.0.0.tar.gz", hash = "sha256:e07ee6c1d659bc6957034f4800c60cb8b98de798823e34d2a2bba1caa85a4509", size = 171415, upload-time = "2025-09-07T13:22:21.317Z" } wheels = [ @@ -1923,17 +2024,22 @@ wheels = [ [[package]] name = "stemdeck" -version = "0.1.0" +version = "0.3.0a1" source = { editable = "." } dependencies = [ { name = "demucs" }, { name = "fastapi" }, { name = "librosa" }, + { name = "numba", version = "0.61.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, { name = "pyloudnorm" }, { name = "python-multipart" }, { name = "soundfile" }, - { name = "torch" }, - { name = "torchaudio" }, + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "torchaudio", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "torchaudio", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "urllib3" }, { name = "uvicorn", extra = ["standard"] }, { name = "yt-dlp" }, ] @@ -1952,14 +2058,19 @@ requires-dist = [ { name = "fastapi", specifier = ">=0.115" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.27" }, { name = "librosa", specifier = ">=0.10" }, + { name = "numba", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = ">=0.61,<0.62" }, + { name = "numpy", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = "<2" }, { name = "pyloudnorm", specifier = ">=0.1.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24" }, { name = "python-multipart", specifier = ">=0.0.9" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, { name = "soundfile", specifier = ">=0.12" }, - { name = "torch", specifier = ">=2.6,<2.7" }, - { name = "torchaudio", specifier = ">=2.6,<2.7" }, + { name = "torch", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'", specifier = ">=2.6,<2.7" }, + { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = ">=2.2,<2.3" }, + { name = "torchaudio", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'", specifier = ">=2.6,<2.7" }, + { name = "torchaudio", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'", specifier = ">=2.2,<2.3" }, + { name = "urllib3", specifier = ">=2.7.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, { name = "yt-dlp", specifier = ">=2024.12.0" }, ] @@ -2053,16 +2164,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "torch" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "filelock", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "fsspec", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "jinja2", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "sympy", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "typing-extensions", marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/55/7192974ab13e5e5577f45d14ce70d42f5a9a686b4f57bbe8c9ab45c4a61a/torch-2.2.2-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:b2e2200b245bd9f263a0d41b6a2dab69c4aca635a01b30cca78064b0ef5b109e", size = 150788930, upload-time = "2024-03-27T21:08:09.98Z" }, + { url = "https://files.pythonhosted.org/packages/3f/14/e105b8ef6d324e789c1589e95cb0ab63f3e07c2216d68b1178b7c21b7d2a/torch-2.2.2-cp311-none-macosx_10_9_x86_64.whl", hash = "sha256:95b9b44f3bcebd8b6cd8d37ec802048c872d9c567ba52c894bba90863a439059", size = 150796474, upload-time = "2024-03-27T21:09:29.142Z" }, + { url = "https://files.pythonhosted.org/packages/79/78/29dcab24a344ffd9ee9549ec0ab2c7885c13df61cde4c65836ee275efaeb/torch-2.2.2-cp312-none-macosx_10_9_x86_64.whl", hash = "sha256:eb4d6e9d3663e26cd27dc3ad266b34445a16b54908e74725adb241aa56987533", size = 150797270, upload-time = "2024-03-27T21:08:29.623Z" }, +] + [[package]] name = "torch" version = "2.6.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "filelock", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "fsspec", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "jinja2", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')" }, { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, @@ -2076,10 +2218,10 @@ dependencies = [ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, + { name = "setuptools", marker = "(python_full_version >= '3.12' and platform_machine != 'x86_64') or (python_full_version >= '3.12' and sys_platform != 'darwin')" }, + { name = "sympy", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/37/81/aa9ab58ec10264c1abe62c8b73f5086c3c558885d6beecebf699f0dbeaeb/torch-2.6.0-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6860df13d9911ac158f4c44031609700e1eba07916fff62e21e6ffa0a9e01961", size = 766685561, upload-time = "2025-01-29T16:19:12.12Z" }, @@ -2100,12 +2242,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/8b/d60c0491ab63634763be1537ad488694d316ddc4a20eaadd639cedc53971/torch-2.6.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:ff96f4038f8af9f7ec4231710ed4549da1bdebad95923953a25045dcf6fd87e2", size = 66536783, upload-time = "2025-01-29T16:22:08.559Z" }, ] +[[package]] +name = "torchaudio" +version = "2.2.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "torch", version = "2.2.2", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/70/ca793994d37815070f6b53932b71822f66cfb3e197e6937426815998221e/torchaudio-2.2.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b1d58201d108e85db3e35b84319f33884f61f327c38ead86913218c8c1acc3dd", size = 3398751, upload-time = "2024-03-27T21:12:31.998Z" }, + { url = "https://files.pythonhosted.org/packages/57/c4/80cc3315dd1ca706643b78f894901d4d888ffe376a5e401f73d9db61071e/torchaudio-2.2.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:f1a81a518a3e86c004125eb891fc433ce8fb2343295b5d612d0f37b24e131efd", size = 3405553, upload-time = "2024-03-27T21:12:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/05/39/fcc68b1f848a38b57446b624be42db66fec3587972941a5b86fc19b8bd45/torchaudio-2.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:da3cc523696166ea525d2b3377d789da5388f36d94a20a324b09df00f1c43458", size = 3400705, upload-time = "2024-03-27T21:12:10.303Z" }, +] + [[package]] name = "torchaudio" version = "2.6.0" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.13' and platform_machine != 'x86_64') or (python_full_version >= '3.13' and sys_platform != 'darwin')", + "(python_full_version == '3.12.*' and platform_machine != 'x86_64') or (python_full_version == '3.12.*' and sys_platform != 'darwin')", + "(python_full_version == '3.11.*' and platform_machine != 'x86_64') or (python_full_version == '3.11.*' and sys_platform != 'darwin')", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] dependencies = [ - { name = "torch" }, + { name = "torch", version = "2.6.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/38/aa/f634960ac094e3fc6869f5c214ccfa6f74da2b1a89cefac024f6c650a717/torchaudio-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0eda1cd876f44fc014dc04aa680db2fa355a83df5d834398db6dd5f5cd911f4c", size = 1808471, upload-time = "2025-01-29T16:29:43.783Z" }, @@ -2181,11 +2348,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]]