Files
stemify/server/README.md
T
2026-10-20 15:42:55 -07:00

7.3 KiB

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:

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:

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

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:

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 1 0 = never scan the library, only do Stem Now requests
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.