main
28 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9b655d5a07 |
Windows: leaner portable package and opt-in in-app updater (#421) (#423)
* feat(windows): leaner portable package and opt-in in-app updater (#421) Issue #421 asked for Python embedded in a single EXE so updating would not mean copying ~20k loose files over an existing install. A onefile EXE is not viable for this stack (onefile modes re-extract the whole multi-GB payload on every launch, and torch/onnxruntime fight frozen-import hooks), so this addresses the root cause instead: ship less, and stop making users hand-copy a full zip for a release that only changed app code. Leaner package (make-portable.ps1): - Stripping is now unconditional. The -StripVenv opt-in gate was a silent regression risk: nothing stopped a future workflow edit from shipping the unstripped venv with no error. - Also strips stdlib base/Lib/test and per-package test/tests dirs. - Deliberately does NOT strip .dist-info/RECORD. pip needs it to replace a package, and install_cuda_torch pip-installs into this venv on every NVIDIA machine at first run; removing it yields "Failed to uninstall ... missing RECORD file". - Adds a post-strip import check so an over-aggressive strip fails the build rather than a release. Updater (main.rs, catalog.js): - New commands installed_runtime_id, download_app_update, apply_app_update. - Opt-in: the check on launch is unchanged, but download and apply are each an explicit click. It never auto-applies and never interrupts a running job. - Replaces StemDeck.exe and backend/ only. python/ is never touched, because an NVIDIA install rewrites it with CUDA torch at first run and torchDeviceSettled skips ensure_torch_device once the device is cuda, so swapping the directory would silently drop that machine to CPU with no recovery. - The runtime id (uv.lock + interpreter major.minor) is a compatibility gate, not a download trigger: if a release changed the Python dependency set the updater stands down and points at the full download. Only 19 of the last 200 commits touch uv.lock, so the fast path covers most releases. - apply_app_update stages and validates everything before any destructive rename, stops the backend synchronously first (the existing stop_backend returns before the process dies, which would have made every update fail on Windows), and retries renames past transient AV/indexer handles. - Known gap, documented in code: the two exe renames are not atomic. A hard crash in that window leaves StemDeck.exe.old needing a manual rename. Closing it needs a bootstrap launcher that is never itself replaced. CI publishes -app.zip, its .sha256 and -runtime-version.json alongside the unchanged full zips. Fresh installs are unaffected. i18n: the 5 new strings are translated into all 7 language tables, not just English. t() falls back to English silently, so an English-only key looks correct in testing and ships untranslated to six locales. Verified: Windows and Linux (WSL) both compile clean with no new clippy warnings, 39 Rust tests pass on both, JS suites pass, ruff clean. Two new unit tests pin the JSON contract between the PowerShell writer and the Rust reader. Not yet verified: no end-to-end run against a real release. * fix(updater): make the in-app update actually work, verified end to end (#421) Built both packages on a real Windows box and drove the whole flow. Four bugs that only surfaced by running it, none of which static checks could see. 1. Stale version after updating. app_version() read installed dist metadata, which lives in python/ -- the directory the updater deliberately never replaces. A self-updated install kept reporting the old version and would re-offer an update it had already applied, forever. It now prefers the app layer's static/version.json, which moves with backend/. Gitignored, so Docker and source checkouts still fall through to the hatch-vcs metadata. Proven: after a real update, python/ dist-info says 0.13.0 while /api/health reports 0.13.1. 2. The page CSP blocked the whole feature. The UI is served over http by the Python backend, so its connect-src applies: api.github.com is allowed, github.com and objects.githubusercontent.com are not, and that is where release assets live. Fetching the checksum and runtime id from JS was refused, so the pill would simply never appear. Those two reads moved into Rust (check_app_update), whose HTTP client is not bound by the page CSP, so the policy from #171 stays exactly as tight as it was. 3. plugin:event|listen refused by the Tauri ACL. App-defined commands are not ACL-gated but plugin commands are, and the capability does not cover the remote http origin the UI is served from. The progress bar is now indeterminate instead of granting a remote origin event permissions to put a percentage on a 5 MB download. 4. The post-strip import check re-bloated the package. Running Python regenerated 1,912 files / 39 MB of __pycache__ that the strip had just removed, cancelling nearly all of it: the net saving was 180 files. Swept once after the last interpreter run, and backend/ no longer ships a developer's local __pycache__ either. Also: the *.old sweep now runs on every launch rather than only on a version change. apply_app_update relaunches then exits, so on the first launch of the new build Windows still holds StemDeck.exe.old open, the delete fails silently, and gated on a change that already happened it would never retry. Observed for real: 15.7 MB stranded. Verified swept on the next launch. UI: "Update now" is an accent pill BESIDE Download, not a replacement, so the zip stays one click away and is the escape hatch if an update fails. Measured against the published v0.13.0 package: 18,143 -> 16,056 files (-2,087, -11.5%) and 883 -> 850 MB. The real win for #421 is the update path itself: 5 MB and 123 files instead of 284 MB and 16,056. Verified on this machine: a real 6-stem Demucs separation through the stripped package; the full notify -> Update now -> download -> restart -> relaunch cycle, after which user data (job, 7 stems, 130 MB of models), portable.txt, cpu-only and python/ were all untouched; and the safety gate correctly declining, with no download attempted, when the release's runtime id differs. Not covered: the NVIDIA package was not built, though the risk that motivated the gate is structurally gone now that python/ is never swapped. * fix: address code-quality review on the version-source change (#421) Both findings from the automated review were fair. Narrow the bare `except Exception: pass` in app_version() to (OSError, ValueError, AttributeError). That is bandit B110, which this repo's own security conventions call out. The three cover every real failure here -- absent or unreadable file, invalid JSON or bad encoding, and valid JSON that is not an object so has no .get -- while letting an actual bug in the function surface instead of silently degrading the reported version. Bandit now reports no issues for the file. Use one import style in test_health_api.py so app.main is no longer imported both as `import app.main as main` and `from app.main import app` in the same module. Also added a "[]" case: JSON that parses but is not an object, which is the AttributeError branch the narrowed except now names explicitly. * feat(updater): extend the in-app update to Linux (#421) Linux ships the same shape as Windows -- executable, backend/ and python/ side by side -- so the updater generalises rather than needing a second design. The platform-specific parts are now three small seams: the archive format, the executable name, and one new gate. Rust: - widen the updater's cfg gates from `windows` to `any(windows, linux)`, and replace extract_zip_archive with extract_update_archive, which uses zip on Windows and the existing extract_tar_archive on Linux - APP_EXE_NAME so the swap and the leftover sweep stop hardcoding StemDeck.exe - stop_backend_and_wait now sends SIGTERM and waits before escalating on unix, matching what stop_backend already does on window close - new app_root_is_writable gate: packaging/linux/install.sh offers a global install into /opt/stemdeck, which is root-owned while the app runs as the user. check_app_update declines up front rather than failing part way through a swap. Windows portable installs are user-writable by construction, but the probe is cheap and honest on both. tar rather than zip on Linux is deliberate: it preserves the executable bit. A zip would land StemDeck without +x and the relaunch after an update would fail with a permission error. Packaging (scripts/linux/make-portable.sh): - write python/runtime-version.json using the same uv.lock + interpreter major.minor formula as the Windows script, so the compatibility gate behaves identically on both - bring the strip to parity: stdlib test/, per-package test/tests, a post-strip import check, and a final __pycache__ sweep after the last interpreter run - PUBLISH_UPDATER_ASSETS=1 emits the slim app-layer tarball, its checksum and the runtime marker; wired into the CPU build in linux-release.yml since StemDeck and backend/ are identical between both variants Frontend: updaterAssetNames() maps the platform to its asset names, and the wiring is gated on that rather than on os === "windows". macOS is deliberately still excluded, and the comments now say why rather than just that it is: backend_dir() resolves the backend inside the downloaded runtime pack rather than the .app, so its app layer is a different thing and the existing runtime-pack updater already covers most of it. Verified: both platforms compile clean with no new clippy warnings, 42 tests on Windows and 43 on Linux (the extra one is the read-only-root gate, which is meaningless on Windows). The app-layer archive was round-tripped on Linux to confirm it contains exactly StemDeck + backend/, that python/ does not leak into it, that the executable bit survives, and that replacing a running binary works. Not yet run end to end against a real Linux release. * fix(updater): see pre-releases, and compile the Rust in CI (#421) Two gaps that would each have undermined the update flow on release day. The update check polled /releases/latest, which GitHub defines as the most recent NON-PRERELEASE, non-draft release. Ship a version with the pre-release box ticked and it becomes invisible: no notification, no update button, on any platform, with nothing in the logs to explain it. StemDeck has always published even its alphas as normal releases (v0.8.0-alpha.17 has prerelease=false), which is the only reason this has not bitten yet -- it was a trap waiting on someone ticking a box. Now polls the releases list and takes the newest non-draft, so it is correct either way. Drafts stay excluded: they are already invisible unauthenticated, and a maintainer should not be offered a release whose assets do not exist yet. windows-check.yml and macos-check.yml now also run on pull requests that touch desktop/src-tauri/**, not workflow_dispatch only. This PR added roughly 600 lines of mostly cfg-gated Rust across two commits and every CI check passed without compiling a single line of it; the comment at the top of windows-check.yml notes that exact gap already shipped a broken Windows build in v0.11.1's first release attempt. Scoped by path so the self-hosted runners see no extra load from the majority of PRs, which never go near src-tauri. This also gets the macOS branch compiled for the first time. Local verification covered Windows and Linux, so the cfg(not(any(windows, linux))) arm of the three updater commands has never been near a compiler. * test(e2e): match the releases-list shape the app now polls (#421) The update-check stub returned a single release object, which was right for /releases/latest. The app now polls the releases list so a pre-release is still seen, so the fixture has to return an array or checkForUpdate bails and the release card never appears. Caught by frontend-e2e on the previous commit, which is the suite doing exactly its job: the only assertion that covers this path is report-failure.spec.mjs:98, and it went red immediately. * feat(i18n): add French, and make the runtime id line-ending independent French is a complete table, not a partial one: 435 keys, the same set German and Portuguese carry (English's 443 minus the ten Polish-only .few/.many forms and the bare upload.skippedFiles, plus singular forms for the three playlist.skip.* families). French takes the one/other buckets, so plural() needs no change. Verified with the checks from .claude/rules/i18n.md: the drift check reports clean, and separately there are zero {placeholder} mismatches and zero HTML tag mismatches against English. The 27 strings identical to English are genuinely identical in French (Piano, Solo, Transport, Position, LUFS, Standard, Port, the brand names, CUDA (NVIDIA), MPS (Apple Silicon)). Separately: the runtime id was being computed from the raw bytes of uv.lock, so a Windows checkout with core.autocrlf=true hashed CRLF and Linux hashed LF, and the same lockfile produced two different ids -- caught by building the Linux package and seeing py3.12-d74d6ef80c5e9d1f where Windows had produced py3.12-dbda45e38e1044cf. Each platform stayed self-consistent so the gate still worked, but the id would shift spuriously if a runner's autocrlf ever changed, silently declining app-only updates that were in fact compatible. Both scripts now hash the content with newlines normalised; PowerShell, bash and a reference Python implementation all agree on d74d6ef80c5e9d1f. * chore: pin Unraid template to 0.14.0 Per .claude/rules/unraid-template-version.md this is an explicit decision each time, not a default. Confirmed for this release. The 0.14.0 GHCR image is published by docker-publish.yml when the release is created, so the tag exists shortly after this lands. --------- Co-authored-by: Thales <> |
||
|
|
b50e221ac0 |
fix: pin librosa <1 and add audioread explicitly (#407) (#408)
librosa 1.0.0 dropped its audioread dependency, but audio-separator (vocal split, #275) still imports audioread directly. uv.lock already resolved librosa 0.11.0, so dev/CI never hit this, but the packaging scripts install from pyproject.toml rather than the lockfile, so an unbounded upper bound let the shipped macOS/Windows/Linux runtime packs silently pick up librosa 1.0.0 and lose audioread, breaking only the vocal-split subprocess in the packaged artifact. Also add audio_separator/onnxruntime to all three packaging scripts' import verification, platform-guarded on Intel macOS, so a broken vocal-split dependency chain fails the build instead of shipping. Co-authored-by: Thales <> |
||
|
|
306f2ce913 |
Portable Windows data dir + auto-clear resolved failure notifications (#402)
* Redirect Windows portable zip cache data to data/ next to the exe FFmpeg, Demucs models, config, and logs currently write to %LOCALAPPDATA% regardless of where the zip is extracted, not to the data/ folder the README already describes. A portable.txt marker, shipped in every future Windows zip, switches local_data_dir() to the exe-relative data/ folder that packaging already stages. Jobs/library data is deliberately left untouched: it stays on its existing default (~/Documents/StemDeck) and remains relocatable via the existing Settings -> StemData location picker (#354). Defaulting it into the exe-adjacent folder was the design in an earlier attempt at this fix, and was reverted -- that folder is exactly what a user deletes or overwrites thinking it's disposable. Fixes #399 * Auto-clear failure notifications once they're resolved Failure notifications (import/playback/export/update) persist until manually dismissed, deliberately, from #359 -- so a crash or reload doesn't lose the evidence needed for a bug report. This adds a second, independent trigger on top without touching that: a notification also clears once the thing it was about is actually resolved, while still surviving a plain reload in the meantime. - import: clears when a re-import supersedes the failed track, or when the track is trashed/purged - playback: clears when the same track plays back successfully - export: clears when the same track exports successfully (jobId is snapshotted at click time, not read live at settle time, since settling can take up to EXPORT_BUSY_MAX_MS and the user may have switched tracks by then); log export clears separately, keyed by kind since it has no jobId - update: clears on the next successful check, which in practice only happens on the next app start -- checkForUpdate() has no periodic re-check today Fixes #401 --------- Co-authored-by: Thales <> |
||
|
|
405fec28a1 |
Revert "ci: sign the Windows release executable with SignPath (#394)"
This reverts commit
|
||
|
|
7eba6340ef |
ci: sign the Windows release executable with SignPath (#394)
SignPath's OSS tier requires every job leading up to a signing request to run on a GitHub-hosted agent, but the Windows release runs entirely on the self-hosted runner. Split the workflow instead of moving the whole build: - New `sign-exe` job on `windows-latest` builds only StemDeck.exe, uploads it as a workflow artifact, submits it to SignPath, and republishes the signed binary as an artifact. Version files are stamped before the build because SignPath restricts Foundation projects on PE product name and version. - `build-and-upload` now depends on it, downloads the signed executable, and packages both variants around it via a new `-PrebuiltExe` flag on make-portable.ps1. That also removes the redundant second Rust build the CPU package used to trigger. - make-portable.ps1 rejects an unsigned prebuilt binary before it reaches the zip, and the scan step reports the Authenticode status of what was packaged. - workflow_dispatch entry point plus a release-only guard on the upload step so the integration can be exercised against a test-signing policy without cutting a tag. Requires repo secret SIGNPATH_API_TOKEN and repo variable SIGNPATH_ORGANIZATION_ID. Adds the code signing policy and attribution required by the SignPath Foundation terms. |
||
|
|
5565b216ba |
feat(linux): add an optional installer for desktop integration (#364)
Implements #342. StemDeck stays portable: extract the tarball, run ./StemDeck, and none of this is required. install.sh is there for people who would rather launch from their applications menu. It installs the package it sits in and never downloads anything, so the version and the CPU/NVIDIA variant come from the package itself (backend/static/version.json and the cpu-only marker) and cannot drift from the build being installed. That also removes any need to verify a second download. Design notes, mostly things the reference installer in #342 got wrong: - Install is atomic. The new copy goes to <target>.new and is verified before the old one is moved aside, so a failure partway leaves the working install untouched. Removing the old copy first is what made a failed upgrade in that fork leave the machine with no StemDeck, no launcher and no manifest recording where it had been. - A failed copy cleans up its own staging directory rather than leaving a package-sized partial on disk. - Exec is quoted, so an install path containing a space still launches. - Version comparison is semver-aware. sort -V ranks 0.8.0-alpha.17 above 0.8.0, which would tell every pre-release user they were current the day a stable release shipped. - Reading a missing manifest key yields empty rather than killing the script, which under set -euo pipefail is what a grep|head|cut pipeline does. - Global installs put the launcher in /usr/share/applications and the icon in /usr/share/pixmaps, so other users on the machine can see it. - Installing from inside the destination is refused rather than moving the running script out from under bash. - Non-x86_64 machines get a clear refusal instead of a binary that cannot run. User data is never touched. Stems live in ~/Documents/StemDeck and the runtime, models and logs in $XDG_DATA_HOME/stemdeck, both outside the install directory. Legacy data/ from pre-migration builds is carried across an upgrade, and uninstall refuses to delete it, leaving the folder and saying why. tests/linux/test_install_sh.sh runs the real installer against a synthetic package in a throwaway HOME: 52 checks covering install, upgrade, the failed-upgrade case, uninstall, corrupt manifests, paths with spaces, legacy data, self-install, arch refusal and the semver table. CI runs it on Linux with shellcheck and desktop-file-validate. Closes #361 |
||
|
|
f9d7182f4b |
build(linux): stage desktop-entry assets into the portable tarball (#363)
Prep for the optional Linux installer (#342). Carrying the icon and a .desktop template inside the package is what lets the installer be self-contained: no second download, and no asset URL that can drift from the release being installed. The Tauri icon is already square at 1024x1024, so it doubles as the desktop icon with no separate artwork to keep in sync. Exec= is quoted in the template. The freedesktop spec splits Exec on whitespace, so the unquoted form used by the reference installer in #342 produces an entry that tries to run ".../My" when the user installs to a path like ~/My Apps. It is invisible until someone picks a custom directory, which is why it is pinned by a test. Both variants pick this up: the CPU and NVIDIA packages run the same script with CPU_ONLY toggled. install.sh itself, and the README-LINUX.txt change documenting it, land with #361 -- this commit deliberately ships nothing that references a file which does not exist yet. Closes #360 |
||
|
|
655b0d0e95 |
fix(linux): install CUDA runtime deps with the GPU torch wheel (#324) (#325)
The Linux NVIDIA package could not start its backend at all. Setup detected
the GPU and pip-installed torch==X+cuXXX with --no-deps, mirroring Windows.
But Linux CUDA wheels do not bundle the CUDA runtime -- they dlopen
libcublas/libcudnn/... out of the nvidia-* PyPI packages at import time, and
make-portable.sh strips exactly those packages to keep the tarball under
GitHub's 2 GiB asset cap. The result was a CUDA torch with no CUDA runtime:
ValueError: libcublas.so.*[0-9] not found in the system path
app/main.py imports torch at module scope, so this killed the backend
outright ("backend did not become healthy within 90 seconds") on every
launch, not just GPU work.
- install_cuda_torch now runs a second, dependency-resolving pip pass on
Linux only (cuda_wheel_needs_runtime_deps). Same specs and index, without
--no-deps/--ignore-installed, so pip sees torch as satisfied and installs
only the missing nvidia-* wheels. Windows keeps the single --no-deps swap:
its wheels carry the DLLs in torch/lib and pulling ~2.5 GB of nvidia-*
there would be pure waste. macOS is untouched (MPS path).
- Restore CPU torch when verify_cuda_torch fails. Recording torchDevice=cpu
was never enough -- an unloadable CUDA wheel stays on disk and keeps the
backend from importing torch at all. New reason
"cuda-verify-failed-cpu-restore-failed" when even that fails.
- Extract run_pip_install (PID tracking, 20 min timeout, stderr logging) so
both installs and the restore share one path.
- Add the missing "Linux tar.gz" option to the bug report template, as the
reporter noted.
Co-authored-by: Thales <>
|
||
|
|
8574be99a6 |
build(windows): don't bundle CUDA torch in the NVIDIA package (revert #318) (#320)
#318 force-installed the CUDA torch wheel into the NVIDIA package, which pushed the release zip to 2456 MB -- over GitHub's 2 GiB release-asset cap (2147483648 bytes) -- so the Windows NVIDIA asset upload failed on the v0.8.0-alpha.12 release. Bundling was the wrong approach: the desktop app already installs the CUDA build on first run via ensure_torch_device (which picks the cuXXX index matching the detected GPU's compute capability / driver). The NVIDIA package is meant to ship base torch small (~419 MB zip) and pull CUDA on first launch. #317 is the actual fix -- it makes that first-run install trigger again after a CPU->NVIDIA build swap. Restore the non-CpuOnly path to base torch and document why not to bundle, so this isn't reintroduced. Co-authored-by: Thales <> |
||
|
|
9c3eedd2eb |
build(windows): install CUDA torch explicitly for the NVIDIA package (#316) (#318)
The non-CpuOnly path relied on `pip install .` yielding a CUDA torch, but the Windows PyPI torch wheel is CPU-only (unlike Linux, whose default wheel bundles CUDA). GPU support in the "NVIDIA" package was therefore an implicit property of the build host's pip resolution and could silently regress to CPU. Force-install the cu124 wheel from the PyTorch index so the NVIDIA build is deterministically GPU-capable; the CPU path is likewise made explicit so it downgrades a host that resolved a CUDA wheel. Co-authored-by: Thales <> |
||
|
|
c19d67eb79 |
fix(desktop): NVIDIA build silently falling back to CPU (#247) (#267)
* fix(desktop): NVIDIA build silently falling back to CPU (#247) Three independent defects each land the NVIDIA build on CPU with no visible error and no recovery path: 1. The cpu-only marker was trusted in the shared per-user data dir, not just the app root. The CPU build wrote/migrated that marker there, so anyone who ever ran the CPU build got the NVIDIA build permanently pinned to CPU -- GPU detection never even ran. is_cpu_only_package now checks the app root only; a stale data-dir marker is auto-deleted and logged. 2. A CPU result from a transient failure (no GPU detected, CUDA verify failed) was persisted the same as a real CPU-only package, and the setup gate treated any truthy torchDevice as "done" -- one bad first run pinned CPU forever. Device selection now persists a reason (torchDeviceReason), and the setup gate only treats cuda/mps or a genuine cpu-only package as settled; a failure-born CPU or a legacy install with no reason re-probes the GPU on the next launch. Existing affected installs self-heal on relaunch, no user action needed. 3. nvidia-smi discovery only checked System32 and PATH; some DCH driver installs place it only under DriverStore\FileRepository\nv*\. Added that scan (newest package wins) and raised the first probe's timeout to 30s for Optimus laptops waking a sleeping dGPU. Every detection decision is now logged to setup.log. Also drops the Windows CPU-only portable package's data\cpu-only staging (scripts/windows/make-portable.ps1), which was the source of the poisoned marker. 5 new Rust unit tests cover marker precedence, the self-heal + log line, CPU builds not churning their own marker, and the DriverStore newest-wins scan. * feat(settings): compute device selector for the self-hosted server Companion to the desktop #247 fix, for the server/Docker/Unraid path: device selection was a frozen constant (DEMUCS_DEVICE, computed once at import), so the only override was the STEMDECK_DEMUCS_DEVICE env var plus a restart -- invisible to Docker/Unraid users without container access. - app/core/settings.py: demucs_device setting (auto | cuda | mps | cpu, default auto = hardware probe). Forcing cuda/mps verifies availability BEFORE persisting and rejects with a clear error otherwise -- never persist a device that would silently fall back later (the #247 lesson applied here). STEMDECK_DEMUCS_DEVICE seeds the default so existing env-based deployments keep their forced device. - app/core/config.py: _detect_device -> detect_torch_device (pure hardware probe; env handling moved to the settings seed); DEMUCS_DEVICE constant removed. - app/pipeline/separate.py: reads the device fresh per job -- a Settings change applies to the next separation, no restart. - app/main.py: /api/settings gains demucs_device (choice) and demucs_device_resolved (what jobs will run on); POST validates via the setter (422 with the reason). Startup log and /api/health read live. - static/js/catalog.js: "Compute device" select in Settings -> Advanced, showing the resolved device; a rejected force surfaces the server's reason via showError and reverts the select. Also aligns the port-input fallback with the 8000 default from the earlier port unification. - .docs/improvements/self-hosted-compute-device-setting.md: design doc. 5 new tests: auto-resolution, env seeding, verify-before-persist rejection, unknown-choice rejection, and the API round trip incl. 422 paths. * feat(settings): gray out compute devices this machine can't use The Compute device dropdown now disables options that aren't available or detected (Auto and CPU are always selectable; CUDA/MPS depend on the hardware + torch build), labeling them "— not available" so it's clear why. - config.py: available_torch_devices() returns the usable devices best-first; detect_torch_device() is now its first element (no duplicated torch probe). - settings.py: set_demucs_device verifies against membership in available_torch_devices() rather than only the top pick. - /api/settings: new demucs_devices_available list for the UI. - catalog.js: disable + relabel unavailable <option>s on load and after each change. * fix(ui): settings scrollbar no longer overlaps right-aligned controls The Advanced settings pane scrolls, and its scrollbar drew directly over the right-aligned Port / Compute device controls. Reserve a scrollbar gutter (padding-right + equal negative margin so it sits in the card's existing 12px padding), keeping content aligned with the fixed header/footer. Surfaced once the new Compute device row made the pane tall enough to scroll. |
||
|
|
da93c5ee44 |
feat: export as MP4 (karaoke video) for MP4 uploads and YouTube (#226)
* feat: export as MP4 (karaoke video) for MP4 uploads and YouTube (#219) Add an MP4 export that muxes the current mixer state (e.g. vocals muted) with the source video, producing a karaoke-style video. Backend: - Preserve a silent video.mp4 from .mp4 uploads (stream-copy, no re-encode). - YouTube jobs do a best-effort video-only download (H.264/avc1, <=720p) to video.mp4, decoupled from the audio source so failures degrade to audio-only. New STEMDECK_VIDEO_MAX_HEIGHT config. - GET /api/jobs/{id}/video.mp4 streams a fragmented MP4: the amix audio graph encoded as AAC, video stream-copied. - has_video flag on Job, surfaced in state and persisted to metadata. Frontend: - MP4 added as a fourth export format (WAV/MP3/FLAC/MP4), shown only for jobs with a preserved video track. In MP4 mode, Export Mix produces the karaoke video and the audio-only Stems/Region rows are hidden. SoundCloud and plain audio uploads are audio-only (no MP4 option). * feat: bundle FFmpeg on Linux via first-launch download Linux no longer requires `sudo apt install ffmpeg`. The desktop shell now downloads a static FFmpeg build into the user data dir on first launch (like Windows/macOS), falling back to a system ffmpeg on PATH when present. This also fixes Demucs failing to decode compressed sources, since the download lands in data_dir/ffmpeg which config.json already adds to PATH. - ensure_ffmpeg: prefer a system ffmpeg, else download_linux_ffmpeg. - download_linux_ffmpeg: fetch the .tar.xz, extract with system tar, copy ffmpeg + ffprobe into data_dir/ffmpeg. STEMDECK_FFMPEG_URL overrides. - Widen download_file and make_executable from macos to unix so Linux reuses them. - Not bundled in the tarball, so we don't redistribute FFmpeg. - Update Linux README/notices/packaging comment to drop the ffmpeg apt step. * style: apply ruff format to MP4 export code --------- Co-authored-by: Thales <> |
||
|
|
3995e9236f |
fix: remove orphaned nvidia-* CUDA packages from Linux bundle (#225)
Both Linux tarballs were 2.5 GB (over GitHub's 2 GiB asset limit) even with CPU torch. Root cause: 'uv pip install <project>' pulls the default Linux torch, which is the CUDA build, dragging in nvidia-* runtime packages (cuDNN/cuBLAS/NCCL/...) and triton (~2.5 GB). The CPU torch swap uses --force-reinstall --no-deps, so torch becomes CPU but those CUDA packages stay installed and orphaned, bloating the tarball. Uninstall the nvidia-* packages and triton after the swap. CPU torch does not use them and the NVIDIA variant re-downloads CUDA at first run. Co-authored-by: Thales <> |
||
|
|
421f2b344c |
fix: ship CPU torch in Linux NVIDIA variant; download CUDA at first run (#222)
The Linux NVIDIA tarball baked the full CUDA torch wheel, producing an asset >2 GiB that GitHub release uploads reject (size must be < 2147483648). On Linux the default PyPI torch wheel bundles the CUDA runtime (~2.5 GB), unlike Windows where the default wheel is CPU-only. The Windows NVIDIA package therefore never baked CUDA -- it ships CPU torch and downloads the CUDA wheel at first run via the desktop shell (install_cuda_torch, which is cfg(not(macos)) and already covers Linux). Mirror that on Linux: bake the small CPU torch in both variants; the NVIDIA variant differs only by omitting the cpu-only marker, so the shell detects the GPU and downloads CUDA on first launch. Keeps both tarballs well under the 2 GiB limit. Co-authored-by: Thales <> |
||
|
|
8131900d0a |
feat: Linux portable builds (CPU + NVIDIA) and release workflow (#220)
* feat: add CPU-only Linux portable build and release workflow
Adds a Linux .tar.gz portable package mirroring the existing Windows/macOS
build paths. Bundles a python-build-standalone runtime (CPU torch + demucs)
plus the Tauri binary so users extract and run ./StemDeck.
- scripts/linux/make-portable.sh: stages PBS Python, force-installs CPU-only
torch, builds the Tauri binary, and produces StemDeck-Linux-x64.tar.gz with
the backend/app + python/ layout find_repo_root resolves at runtime.
- .github/workflows/linux-release.yml: builds on hosted ubuntu-latest on
release publish; installs Tauri v2 apt deps + uv, ClamAV-scans, uploads.
- packaging/linux/{README-LINUX,THIRD_PARTY_NOTICES}.txt: extract-and-run
instructions noting ffmpeg + WebKitGTK are system (apt) prerequisites.
FFmpeg is not bundled: the Linux shell expects ffmpeg on PATH. NVIDIA/CUDA
and AppImage variants are intentionally deferred to later phases.
* fix: don't set PYTHONHOME on Linux (breaks PBS stdlib resolution)
The Linux backend failed to start with 'ModuleNotFoundError: No module
named encodings'. PYTHONHOME was being set to python/bin instead of the
prefix python/, so CPython looked for its stdlib under python/bin/lib and
could not boot.
Linux bundles python-build-standalone exactly like macOS, which detects
its own prefix by walking up from bin/ and must NOT have PYTHONHOME set.
The two PYTHONHOME sites were gated #[cfg(not(target_os = "macos"))],
wrongly including Linux alongside Windows. Only Windows -- whose portable
venv keeps the stdlib under base/Lib -- needs PYTHONHOME, so gate both
sites (start_backend and python_stdlib_ok) to #[cfg(windows)].
This also fixes the latent inconsistency where probe_runtime reported
Python ready (python_stdlib_ok set PYTHONHOME=python, the correct prefix)
while start_backend set PYTHONHOME=python/bin and failed.
* feat: add NVIDIA/CUDA Linux portable variant
Adds a second Linux package, StemDeck-Linux-x64.NVIDIA.tar.gz, with
CUDA-enabled torch baked in (mirrors the Windows NVIDIA variant).
- make-portable.sh: CPU_ONLY toggle (default 1). CPU_ONLY=0 keeps the
project's default torch wheel, which on Linux x86_64 is the CUDA build,
and omits the cpu-only marker so the desktop shell detects the GPU and
uses CUDA at runtime. No app-side changes needed -- the CUDA detection/
install path in main.rs is already cfg(not(macos)) and covers Linux.
- linux-release.yml: builds both variants in one job. CPU first (full Tauri
build), then NVIDIA with SKIP_TAURI_BUILD=1 reusing the same binary. Adds
a free-disk-space step (CUDA bundle is several GB) and drops each
uncompressed stage after taring to stay within the hosted runner's disk.
- README-LINUX.txt: documents both variants and the NVIDIA driver
prerequisite (nvidia-smi must work; CUDA runtime is bundled, no toolkit
install needed; falls back to CPU when no GPU).
* ci: run Linux release on self-hosted linux/x64 runner
Targets the org's self-hosted wsl2 runner ([self-hosted, linux, x64])
instead of hosted ubuntu-latest, matching the Windows/macOS release
jobs. Drops the free-disk-space step: it was a hosted-runner workaround
and would needlessly rm system directories on a persistent self-hosted
box (WSL2's virtual disk has ample room for the CUDA bundle).
* ci: add workflow_dispatch test build for Linux release
Lets you run the full two-variant build + ClamAV scan on the self-hosted
runner without publishing a release, to validate the runner toolchain and
the CUDA build. Resolves the version from a manual input (default 0.0.0,
must be valid PEP 440) instead of the branch ref, and skips the upload
step on non-release events.
---------
Co-authored-by: Thales <>
|
||
|
|
6761831abc |
fix: point runtime pack URL at stemdeckapp, not old thcp repo (#199)
CI never sets RELEASE_BASE_URL, so every macOS release manifest baked the old thcp/stemdeck download URL. It only worked via GitHub's transfer redirect, which is outside our control and would 404 if a repo named stemdeck is ever recreated under thcp. Point the default at the repo we own. Future releases only. |
||
|
|
ff66e15e6c |
fix: address open issues #169 (version), #170 (XSS), #173 (SSRF) (#176)
* fix: address open issues #169, #170, #173 #170 — Stored XSS via library folder names: folder.name was interpolated raw into innerHTML in the folder render path. Escape it with the existing esc() helper (catalog.js), matching the track render paths. #173 — SoundCloud SSRF surface: drop the on.soundcloud.com share shortener from the host allowlist (it redirects to arbitrary targets) and add a yt-dlp extractor allowlist (allowed_extractors=[youtube, soundcloud]) so a URL that slips past host validation can't invoke the generic extractor. #169 — Version stuck at 0.6.0-alpha.2 for source/Docker/self-hosted: make the version git-tag-derived via hatch-vcs (pyproject dynamic version, app/_version.py build artifact). app_version() now reads package metadata -> _version.py -> dev placeholder; static/version.json is removed (now a build artifact, gitignored). Install sites pin SETUPTOOLS_SCM_PRETEND_VERSION from the release version so shallow CI clones / Docker (no .git) don't break (Dockerfile, make-runtime-pack.sh, make-portable.ps1). make-app.sh defaults VERSION to `git describe`. Desktop version literals (Cargo.toml, package.json, tauri.conf.json) are now 0.0.0 placeholders stamped from the tag at build. The update-check no longer nags dev/source builds. Tests: 81 passed; ruff clean; app_version derives correctly. * build: exclude generated app/_version.py from ruff The hatch-vcs build hook writes app/_version.py during uv sync, and CI's `ruff format --check app/` tripped on it (it's gitignored but present on disk during lint). Add it to ruff's exclude list. * ci: make git-derived version resilient to CI's shallow clone (#169) CI runs uv sync in every step, which builds the editable package and triggers hatch-vcs/setuptools_scm. On Woodpecker's shallow, tagless clone setuptools_scm raises ("unable to detect version"), failing the lint step before ruff runs (and skipping the rest). - Set SETUPTOOLS_SCM_PRETEND_VERSION=0.0.0 in the uv-based CI steps so the build never invokes git for the version (CI only lints/tests, never ships). - Add hatch-vcs fallback-version as a second safety net for shallow source installs outside CI. Verified: uv sync --frozen --all-extras succeeds with the env set. * feat: validate library folder names (reject symbols/markup) Folder names now accept only letters (any language), digits, spaces, and a small safe punctuation set (- _ ' & ( ) . ,). Names with markup or symbols (e.g. the XSS probe, or ±!@£$%^&*()_+{:"|?><) are rejected on Save with an inline message instead of being created. Complements the render-time escaping from #170 by blocking such names at the source. * feat: raise folder name limit to 100 chars + enforce in validator Bump the editor input maxlength from 48 to 100 and reject over-length names on Save with an inline message (defensive, in case the cap is bypassed). |
||
|
|
5628963e08 |
[codex] Fix portable Python runtime layout (#30)
* Fix portable Python runtime layout * Ignore issue docs * Organize gitignore * ignored * fix: improve local dev build and runtime setup reliability - Remove redundant size check in verify_runtime_archive; SHA256 is sufficient and size varies across zstd builds of identical content - Extend PATH with Homebrew dirs in extract_tar_archive so tar can find zstd when running inside the app bundle - Skip runtime download in setup.js if archive is already present locally - Make DMG Finder AppleScript non-fatal (cosmetic only) - Poll for uvicorn startup log line instead of fixed sleep 1 - Ignore .local/ dev scripts in gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: extract runtime pack in pure Rust, no zstd system dependency Replace the system tar subprocess with native Rust crates (tar + zstd + flate2). macOS ships without a standalone zstd binary so tar would fail on any machine without Homebrew installed, breaking the setup flow for all new users. Pure Rust extraction has zero external dependencies. * fix: re-download runtime archive if local copy fails verification * fix: delete stale runtime archive on checksum mismatch so retry re-downloads * fix: bundle Python stdlib in runtime pack and set PYTHONHOME at launch UV standalone Python has /install as compiled-in base_prefix so the stdlib is unreachable on user machines. Copy the stdlib from the UV Python into the venv lib directory during runtime pack build, then set PYTHONHOME to the venv root at backend launch so Python finds it. * fix: locate Python stdlib via encodings import instead of path guessing The previous approach (exe.parent.parent/lib/pythonX.Y) fails on UV PBS Python builds where the actual stdlib location differs from expectations. Finding encodings via import is reliable regardless of compiled-in prefix. * fix: use copytree(dirs_exist_ok=True) for stdlib copy and add sanity check The item-by-item copy with if-not-exists was silently skipping files. Using copytree with dirs_exist_ok merges stdlib into the venv lib dir atomically. Added post-copy check that fails the build if encodings is missing so this can never ship silently broken again. * fix: bundle full PBS Python installation instead of venv to include stdlib python -m venv only creates site-packages/ — it relies on sys.base_prefix for stdlib, which is compiled into UV PBS Python as /install (a path that never exists on user machines). Copying the entire PBS installation to runtime/python/ gives us lib/pythonX.Y/ with the full stdlib in place. PYTHONHOME already set in start_backend points Python there at runtime. * fix: remove EXTERNALLY-MANAGED marker from copied PBS Python before installing packages * fix: validate Python stdlib in probe_runtime to detect broken installs |
||
|
|
f3a97bdeb9 |
fix(macos): arch-scope intermediate build paths to fix parallel race conditions
arm64 and x64 builds run in parallel on the same agent/workspace.
Shared staging dirs and manifest files caused rm -rf collisions and
last-writer-wins overwrites. Scope all intermediates by ARCH:
runtime-staging -> runtime-staging-{ARCH}
runtime-manifest.json -> runtime-manifest-{ARCH}.json
app-path.txt -> app-path-{ARCH}.txt
dmg-staging -> dmg-staging-{ARCH}
|
||
|
|
03e1cec1c3 |
fix(macos): replace pip self-upgrade with uv pip install in runtime pack
ensurepip installs a vendored pip then pip --upgrade partially overwrites it, leaving mixed-version files. Subprocess builds then hit ModuleNotFoundError on pip._internal.pyproject (removed in pip 22+). uv pip install bypasses pip subprocesses entirely. |
||
|
|
3ac121438c | fix(macos): force CI=true before tauri build to avoid Woodpecker CI parse error | ||
|
|
f08b6367b8 |
feat: macOS native app v0.4.0-alpha.1 (#27)
* ignore build files. * feat: macOS native app — Tauri shell, runtime pack, MPS, CI pipeline - Tauri 2 macOS app with setup wizard that downloads and installs a self-contained Python/backend runtime pack on first launch - Runtime pack download now streams via reqwest with real-time progress events (runtime-download-progress) replacing the blocking curl call; progress bar shown in setup UI with indeterminate fallback - MPS (Apple Silicon) GPU detection and torch device selection; CUDA path gated to non-macOS targets - macOS data dir uses ~/Library/Application Support/StemDeck - macOS FFmpeg downloaded from evermeet.cx and extracted from zip - Backend watchdog: Python process exits when parent Tauri PID disappears - CloseRequested handler stops backend and exits cleanly - Woodpecker CI pipeline for macOS: arm64 and x64 builds in parallel, DMG inspection, artifact upload to GitHub releases - Build scripts: make-runtime-pack.sh, make-app.sh, make-dmg.sh, make-iconset.sh with LOCAL_DEV_TEST version default - Version stamped from CI_COMMIT_TAG (or LOCAL_DEV_TEST) at build time across Cargo.toml, tauri.conf.json, package.json - SVG logo assets, DMG packaging files, macOS README and notices - README updated with macOS download and build instructions - Bump version to 0.4.0-alpha.1 * fix(security): bump urllib3 to 2.7.0, ignore unresolvable torch x86 CVE - urllib3 2.7.0 fixes CVE-2026-44431 (header forwarding) and CVE-2026-44432 (decompression-bomb bypass) - CVE-2025-32434 (torch 2.2.2 RCE via torch.load) added to .trivyignore: no 2.6.x macOS x86_64 wheels exist; StemDeck has no untrusted torch.load path - Wire .trivyignore into the trivy-fs CI step * ci: trigger macOS and Windows builds on tag push and release Adds event: tag so that pushing a version tag (e.g. v0.4.0-alpha.1 for a pre-release) triggers the build and inspect pipeline. Upload steps remain gated on event: release only. * ci: skip asset upload if already present on pre-release promotion When a pre-release is promoted to latest, GitHub fires a second release event. The upload step now checks if assets already exist for the tag and exits early, preventing --clobber from deleting and re-uploading artifacts during the promotion window. |
||
|
|
e3a2e26062 | Fix Windows release Tauri build invocation | ||
|
|
89b1fcd2e8 |
Fix Windows setup hang after data reset (#14)
* fix(windows): prevent setup hang after data reset * fix(ci): pin workflows to matching woodpecker agents * fix(setup): show progress during long device setup * fix(desktop): surface backend startup logs * fix(setup): keep backend step active until healthy * ci: note clamav scan in release notes * ci: make windows artifact scan logs verbose * fix(windows-build): guard against stale skipped tauri build * Build Windows CPU release independently * Fix Windows portable runtime and version display * Fix backend import ordering --------- Co-authored-by: Thales <> |
||
|
|
5a6493e690 | fix(windows-build): set rustup default stable before tauri build | ||
|
|
7ccf5ae97d | fix(windows-build): override CI=true before tauri build (Woodpecker sets CI=woodpecker which tauri rejects) | ||
|
|
ba7bbe3b5a | fix(windows-build): propagate native command exit codes in make-portable.ps1 | ||
|
|
5b251ccc3b |
Windows portable app: dual CPU/NVIDIA builds, DAW UI improvements (#5)
* Add Windows portable launcher scaffold * readme * readme update * fix * star thistory theme changed * feat: responsive layout, parallel setup flow, and window constraints - Enforce 1440×900 minimum window size in tauri.conf.json - Rewrite setup.js: parallel workspace+gpu phase, minDelay() for guaranteed state visibility, IIFE chains, error cleanup on failure - Add setup.css step indicators (pending/active/done/error) with gold spinner, green checkmark, red X - Fix stems-panel overflow into transport footer (align-self + height) - Make transport, appbar, and wave editor fully responsive with clamp() and fr-based grid columns - Remove dead .stem-list span.hidden rule (covered by base.css) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: ignore data/ directory (runtime-generated) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: untrack .docs directory from git * Add Windows portable release workflow * fix(tests): rewrite yoda conditions in test_config.py * ci: set UV_LINK_MODE=copy to suppress hardlink warning * fix: security hardening, reliability fixes, and observability improvements - Enforce MAX_PENDING_JOBS cap on job submissions (503 when queue full) - Add done_callback to pipeline task to log any unhandled exceptions - Add job_id regex validation to DELETE endpoint - Sanitize pipeline error messages sent to clients (full detail stays server-side) - Move sweep_old_jobs to hourly background task via lifespan (not per-submission) - Add demucs stall watchdog: terminate if no stderr output for 30min - Add SSE connection max lifetime (4h) to prevent zombie connections - Replace shutil.rmtree(ignore_errors=True) with logged _rmtree helper - Fix log levels: chroma/key diagnostics downgraded from WARNING to DEBUG - Add bounds clamping for MAX_DURATION_SEC, JOB_TTL_SECONDS, MAX_PENDING_JOBS - Remove filesystem paths from /health endpoint response - Replace innerHTML with safe DOM construction for BPM and confidence in JS * fix(events): use get_running_loop() instead of deprecated get_event_loop() * fix(tests): update assertions to match sanitized error and health response * star history * feat(desktop): Windows portable app — dual CPU/NVIDIA builds, external links, transport colors - Move frontend to desktop/ui/ and fix frontendDist to point there (fixes Tauri build) - Dual portable zip variants: StemDeck-Windows-x64 (CPU) and StemDeck-Windows-x64.NVIDIA - Strip torch .lib static libraries in StripVenv (-623 MB dnnl.lib alone) - Force-reinstall CPU torch after main pip install to prevent CUDA wheel override - Sentinel file data/cpu-only short-circuits GPU detection in ensure_torch_device() - Add open_url Tauri command + JS click interceptor for Help/Tip external links - Rename GPU setup step to "Configuring compute device" (accurate for both variants) - Play button turns green when active, stop button turns red when pressed - Update CI pipeline for dual Windows variants with manual branch trigger support - Update README with Windows desktop app section and download variant guidance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix mixer volume updates getting stuck Clamp per-track mixer output to the browser-safe 0..1 range and isolate volume write failures so one hot fader cannot leave other channels stale. Keep the existing master-volume fallback when no master fader is present. * fix(ui): DAW view — tracks fill window height, icons aligned per row - stem-waveform-layer: top: 0 (waves-column already starts below ruler, previous top: 72px pushed waveforms 72px too far down) - stems-panel: align-self stretch + margin-bottom 6px to match waves-column height exactly, keeping icon rows in sync with waveform rows - ResizeObserver on waveScroll recalculates --wave-playhead-h and multitrack pxPerSec on every container resize - Commit desktop/package-lock.json for reproducible npm ci in CI pipeline * image:local added for windows builds --------- Co-authored-by: Thales <> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |