Files
2026-10-29 10:28:35 -07:00

140 lines
7.6 KiB
Markdown

# stemserve
Serves the Stemify stems folder to the Android app: an index of every separated
song, the stem files with HTTP range support (so the phone can seek and stream), per-stem
waveforms, and optional Opus transcodes so cached songs take ~1/8 the space.
One Python file, standard library only. `ffmpeg` is optional; without it you get FLAC
only and no waveforms.
## Run it on unraid
The stems live at `/mnt/user/Music/Stems` on the server (`//192.168.86.2/Music/Stems`
from the desktop). From an unraid terminal:
```sh
mkdir -p /mnt/user/appdata/stemserve/cache /mnt/user/appdata/stemd/state/queue
chown -R 99:100 /mnt/user/appdata/stemserve /mnt/user/appdata/stemd
mkdir -p /mnt/user/appdata/stemserve/src # needs server/ and web/ from the repo
cp -r /path/to/stemify/{server,web} /mnt/user/appdata/stemserve/src/ # or copy over SMB
docker build -f /mnt/user/appdata/stemserve/src/server/Dockerfile -t stemserve /mnt/user/appdata/stemserve/src
docker run -d --name stemserve --restart unless-stopped -p 8765:8765 \
-v /mnt/user/Music/Stems:/stems:ro \
-v /mnt/user/appdata/stemserve/cache:/cache \
-v /mnt/user/appdata/stemd/state/queue:/queue \
stemserve
```
or `docker compose up -d --build` in this folder (Compose Manager plugin).
Check it from any machine on the LAN:
```sh
curl http://192.168.86.2:8765/api/info
```
## Settings (environment)
| Variable | Default | Meaning |
|-------------|----------|------------------------------------------------------------|
| `STEMS_DIR` | `/stems` | The Stemify library output folder |
| `CACHE_DIR` | `/cache` | Opus transcodes and waveforms (safe to delete) |
| `PORT` | `8765` | Listen port |
| `TOKEN` | empty | If set, requests need `X-Stem-Token` or `?token=` |
| `INDEX_TTL` | `30` | Seconds between rescans of the stems folder |
| `QUEUE_DIR` | `/queue` | stemd's Stem Now folder (`stemd/state/queue` on unraid) |
| `PLEX_URL` | empty | Plex address; the web UI reaches it through `/plex/` on this server, so HTTPS pages work |
| `PLEX_TOKEN`| empty | Plex token for the web UI (set `TOKEN` too) |
| `WEB` | `1` | `0` turns off the web UI |
## API
| Request | Returns |
|---------------------------------------------------|-------------------------------------------------|
| `GET /api/info` | track count, whether ffmpeg is available |
| `GET /api/index` | every finished song (ETag; 304 when unchanged) |
| `GET /api/peaks?key=K&n=256` | RMS per stem in `n` buckets, 0..1000 |
| `POST /api/prepare?key=K&fmt=opus&br=256` | starts the Opus transcodes in the background |
| `POST /api/stem` `{"paths": [plex file paths]}` | Stem Now: stemd separates these next |
| `GET /file/K/<file>.flac[?fmt=opus&br=256]` | the stem, with `Range` support |
`K` is the song's folder under the stems root, e.g. `1970 - Paranoid/1-02 - Paranoid`.
The app matches Plex tracks to it by path: the stems folder mirrors the music folder.
## Run it anywhere else
```sh
STEMS_DIR=/var/mnt/unraid/Music/Stems CACHE_DIR=~/.cache/stemserve python3 stemserve.py
```
# stemd: separate new music on the server's GPU
`stemd.py` keeps the stems library up to date. It walks the music folder, separates every
track whose stems folder has no `.stemify.json` yet (BS-RoFormer SW 6-stem by default,
like the desktop app), and writes them in the layout above, so stemserve and the phone pick
them up within `INDEX_TTL`. Once it has caught up it checks for new music every 30 minutes.
Nothing runs on the desktop.
It writes the same layout the desktop app's old library mode did (`stemify/library_core.py`),
so the ~300 tracks already done on the desktop are skipped, and a track interrupted
mid-way (`docker stop`, a reboot) is redone from scratch next time.
## Run it on unraid (NVIDIA)
Needs the **Nvidia-Driver** plugin (Apps tab). The image builds from the repo root
because it uses the `stemify` package. Copy the repo, or at least `stemify/`,
`server/` and `.dockerignore`, to `/mnt/user/appdata/stemd/src`, then from an unraid terminal:
```sh
mkdir -p /mnt/user/appdata/stemd/models /mnt/user/appdata/stemd/state
chown -R 99:100 /mnt/user/appdata/stemd
docker build -f /mnt/user/appdata/stemd/src/server/stemd.Dockerfile -t stemd /mnt/user/appdata/stemd/src
docker run -d --name stemd --restart unless-stopped --stop-timeout 60 \
--runtime=nvidia -e NVIDIA_VISIBLE_DEVICES=all -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \
-v "/mnt/user/Music/Actual music/Flacs":/music:ro \
-v /mnt/user/Music/Stems:/stems \
-v /mnt/user/appdata/stemd/models:/models \
-v /mnt/user/appdata/stemd/state:/state \
stemd
docker logs -f stemd
```
The first track downloads the model (~700 MB) into `models/`. Each scan logs how many
tracks are left, and each track logs its name, how long it took and how many times
realtime that was.
Separation runs one track at a time at low CPU priority, and the model is unloaded between
passes so the VRAM is free while idle. It can share the card with Plex transcoding; if
VRAM runs out mid-track, that track is retried on the next pass rather than marked failed.
Tracks that fail (corrupt files, over 40 minutes) are listed in `state/failed.json` and
retried when the file changes; delete that file to retry them all.
**Stem Now** (hold a song, album, artist or playlist in the app) jumps the queue: stemserve
writes the request into `state/queue/`, and stemd separates those tracks next, after the
one it's on (or within 5 seconds if it's idle). Requests run oldest first, and survive a
restart. stemd matches the Plex paths to files under `/music` by their artist/album/file
tail, so the two containers don't need the same mount points. Tracks in `failed.json` are
skipped here too.
To update after changing the code: copy the files over again, rebuild, then
`docker rm -f stemd` and repeat the `docker run`.
Coming from before the rename to Stemify: update stemd first. On its first start it renames
the old done-markers in the stems folder to `.stemify.json` (it logs how many), so nothing is
separated twice. The new stemserve only looks for the new name, so until stemd has done that
the app would briefly see no stems.
| Variable | Default | Meaning |
|--------------|------------------|------------------------------------------------------|
| `MODEL` | `roformer_bs_sw` | any id from `stemify/catalog.py` |
| `DEVICE` | `cuda` | `cuda`, `cuda:N` or `cpu` |
| `RESCAN_MIN` | `30` | minutes between scans once caught up |
| `AUTO_SCAN` | `0` | `1` = scan the library every `RESCAN_MIN`; `0` = only do Stem Now requests |
| `PRUNE` | `1` | Remove stems whose song is gone from the library, at start and every `RESCAN_MIN` |
| `PRUNE_CHANGED` | `0` | Also remove stems of songs replaced in place (size or mtime changed) |
| `PRUNE_MAX` | `0.2` | Skip a prune that would remove more than this share of the stems. `server/prune.py` is the manual dry run, `--apply` to remove |
| `FP16` | `1` | half precision on the GPU (`0` for fp32) |
| `OVERLAP`, `SEGMENT`, `SHIFTS` | `0.25`, `0`, `0` | as in the desktop app's settings |
`docker build --build-arg TORCH_INDEX=cpu ...` makes a small image without CUDA for testing.