10 Commits

Author SHA1 Message Date
Tha.Les 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 <>
2026-08-21 01:00:21 +01:00
Thales 405fec28a1 Revert "ci: sign the Windows release executable with SignPath (#394)"
This reverts commit 7eba6340ef.
2026-08-20 22:12:16 +01:00
Tha.Les 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.
2026-08-19 10:43:31 +01:00
Tha.Les 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
2026-08-12 19:08:49 +01:00
Tha.Les 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
2026-08-12 18:54:15 +01:00
Tha.Les 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 <>
2026-06-25 22:46:23 +01:00
Tha.Les 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 <>
2026-06-24 19:17:37 +01:00
Tha.Les 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 <>
2026-06-24 18:07:34 +01:00
Tha.Les 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.
2026-05-11 23:29:17 +01:00
Tha.Les 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>
2026-05-07 17:06:53 +01:00