Local-GPU stem separation and mixing studio.
Drop in a track, get six sample-aligned stems, mix them in the browser, and export lossless.
English · 한국어
StemFlow uploads an audio file into an isolated UUID job, runs a two-stage BS-Roformer v4 cascade on a local CUDA GPU, and exposes six sample-aligned stems in a Web Audio mixer. From there you can solo, mute, balance, and export — either the lossless stems or a freshly rendered 24-bit mix.
flowchart LR
A[Upload<br/>up to 500 MB] --> B[UUID job<br/>metadata.json]
B --> C[GPU queue<br/>single worker]
C --> D[Stage 1<br/>BS-Roformer ep_317]
D --> E[Stage 2<br/>BS-Roformer-SW]
E --> F[Residual<br/>reconstruction]
F --> G[Float WAV + MP3<br/>+ ZIP]
G --> H[Browser mixer<br/>Web Audio]
H --> I[Export stems<br/>or 24-bit mix]
| 🎚️ Six stems | vocals · drums · bass · guitar · piano · other — all sample-aligned |
| ⚡ Local GPU | Single-GPU background queue with restart recovery and cancellation |
| 🔊 Browser mixer | Sample-synchronized Web Audio transport: seek, mute, solo, master volume, data-derived waveforms |
| 💾 Lossless out | Float WAV masters, MP3 previews for the browser, ZIP packaging, server-side 24-bit mix rendering |
| 📦 Isolated jobs | Persistent UUID directories with atomic metadata.json writes |
| 📈 Live status | Status polling, staged progress, explicit errors, deletion, 24-hour cleanup |
| 🌐 Demo mode | Optional offline walkthrough of the mixer — see Demo audio |
| 🎧 Formats | .mp3 .wav .flac .m4a .aac .ogg, streamed upload with format validation |
Two BS-Roformer models run in cascade, and the sixth stem is computed rather
than inferred. The whole pipeline lives in worker/separate_v4.py;
model inference itself is delegated to the CUDA audio-separator executable, so
the API process only ever needs NumPy and SoundFile.
| # | Step | Input → Output | Progress |
|---|---|---|---|
| 0 | Validate | sf.info on the source; rejects empty files and anything that isn't mono or stereo |
8% |
| 1 | Vocal split — model_bs_roformer_ep_317_sdr_12.9755.ckpt |
source → vocals + instrumental |
12% |
| 2 | Instrument split — BS-Roformer-SW.ckpt |
instrumental → drums, bass, guitar, piano |
46% |
| 3 | Align + residual | five stems + source → six float64 arrays on a common grid | 82% |
| 4 | Write masters | float32 WAV at the source sample rate | 88% |
| 5 | Previews | 192 kbps libmp3lame MP3 per stem |
92% |
| 6 | Package | exports/stems.zip |
97% |
Stage 2 reads stage 1's instrumental, not the original — so vocal bleed is already removed before the instrument model ever sees the audio.
Two model passes over two different files return outputs whose lengths don't necessarily agree. Rather than resampling or padding, the pipeline finds the largest grid all six signals genuinely share and refuses anything that doesn't fit:
length = min(len(mix), *(sf.info(path).frames for path in paths.values()))Every stem is then read at float64 and validated before use — a sample-rate
mismatch, a channel-count mismatch, or an unexpectedly short output raises
instead of being silently patched up. Nothing is stretched, resampled, or
zero-filled, which is what makes the stems line up sample-for-sample in the
browser mixer.
other is never predicted by a model. The instrument model emits its own
leftover stem, and StemFlow discards it — recomputing the remainder against
the true mix instead:
Computed in float64, this makes the invariant exact:
The six stems sum back to the aligned original.
That is a stronger guarantee than a model's own residual output, which only sums
back to whatever it was fed. The trade-off is honest and worth knowing: because
other is a true difference, it absorbs everything the five stems didn't claim —
unmodeled instruments (strings, synths, FX), separation leakage, and the
reconstruction error of both model stages. It's the remainder, not a clean
"everything else" stem.
Masters are written as float32 WAV with no normalization pass, so headroom and
inter-stem levels survive untouched — the residual stays valid after the round trip.
audio-separator does not write a stem whose peak falls below 1e-6 — it logs
it as saved and lists it in the final output summary, but the file never appears.
A track with no piano part is ordinary input, so a missing file is read as
"this stem is silent", not as a failure: the stem is filled with zeros,
written like any other, and named in separation.silentStems.
This matters because a silent stem contributes exactly zero to the sum, so the residual invariant holds unchanged. If the vocal pass leaves no instrumental at all — an a-cappella source — the second model pass is skipped entirely and all four instrument stems are zero-filled.
| Flag | Value | Configurable | Why |
|---|---|---|---|
--sample_rate |
source rate | — | Never resample; the residual requires a shared grid |
--normalization |
1.0 |
pinned | Rescaling would invalidate the residual invariant |
--mdxc_overlap |
8 |
STEMFLOW_MDXC_OVERLAP |
Higher chunk overlap — better seam quality, slower inference |
--use_autocast |
on | STEMFLOW_USE_AUTOCAST |
Mixed-precision CUDA inference; turn off for bit-reproducible runs |
--output_format |
WAV |
— | Lossless hand-off between stages |
Both knobs are read once per job, so changing the environment mid-run can't
make the two model passes disagree. The values used are recorded under
separation in metadata.json, which is what keeps two runs of the same track
comparable:
"separation": {
"vocalModel": "model_bs_roformer_ep_317_sdr_12.9755.ckpt",
"instrumentModel": "BS-Roformer-SW.ckpt",
"mdxcOverlap": 8,
"autocast": true,
"reconstructionError": 1.19e-07,
"silentStems": ["piano"]
}reconstructionError is the peak of |sum(stems) - source| measured on the
float32 data as written to disk, so the invariant is checked against the real
output rather than only in memory. Expect ~1e-7 from the float32 cast; a
materially larger number means something upstream broke the invariant.
Server-side export (server/mix.py) applies each fader in
float64, sums, and writes 24-bit PCM. Levels are validated to 0 ≤ v ≤ 1.5,
and peak normalization only engages if the sum actually exceeds full scale — so
a mix that fits is bit-faithful to the stems:
if peak > 1:
mixed /= peakCancellation is cooperative and checked at every meaningful boundary: between
stages, inside the separator's output-reading loop (terminate → 10 s grace →
kill), and before each stem write and preview encode. Stage directories
_stage1 / _stage2 are removed in a finally block, so a cancelled or failed
job leaves no partial scratch data. Progress and stage strings are written to
metadata.json atomically via a temp-file replace, which is what lets the
queue recover its state across a restart.
Important
StemFlow does not ship the separation engine or the model weights. You
install those yourself, then tell StemFlow where they are. Without them the
studio still runs, but only in demo mode — /api/health reports
modelsReady: false and uploads are refused rather than accepted and failed.
An NVIDIA GPU with CUDA is required. preflight.ps1 asserts
torch.cuda.is_available() and stops if it fails — there is no CPU path in this
project's setup flow, and CPU inference on these models would be impractically
slow for interactive use.
Measured on the reference machine:
| GPU | NVIDIA RTX 5060 Ti, 15.9 GB VRAM, compute capability sm_120 |
| Stack | PyTorch 2.11.0+cu128, CUDA 12.8, cuDNN 9.19 |
| Peak VRAM during separation | 5.8 GB total, ~2.7 GB attributable (3.0 GB was already in use at idle) |
| Speed (12-second stereo 44.1 kHz clip) | ~2 s inference per stage (~4 s total), plus 1–2 s to load each checkpoint; ~21 s end to end through the API |
Both checkpoints are loaded per job, so roughly 4 GB of free VRAM is a sensible floor and 6–8 GB is comfortable.
Only about 4 of those 21 seconds are inference. The rest is fixed per-job
overhead — two separate audio-separator processes each importing PyTorch and
loading a ~700 MB checkpoint, then six MP3 encodes and the ZIP. Inference scales
with track length; the overhead does not, so it dominates on short clips and
matters much less on full songs. It is also why batching many tracks is slower
than the GPU time alone suggests.
Warning
Match the PyTorch build to your GPU. A default pip install torch can
install a CUDA build that does not cover your architecture, producing an
environment that imports fine but cannot use the GPU. This machine's sm_120
(Blackwell) needs CUDA 12.8 or newer. Check with:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_arch_list())"
You need three things, none of which are in this repository:
| # | What | Size | How |
|---|---|---|---|
| 1 | A CUDA audio-separator environment (PyTorch + CUDA) |
~4.8 GB | pip install "audio-separator[gpu]" in its own venv, with a PyTorch build matching your GPU |
| 2 | FFmpeg | ~200 MB | Any recent build |
| 3 | The two BS-Roformer checkpoints plus their .yaml configs |
~1.34 GB | See Models |
Expect roughly 6.5 GB of disk and a one-time download of comparable size.
On a new machine this is real setup work, not a pip install away — the CUDA
environment is the part that takes the longest to get right.
Then point StemFlow at them by copying .env.example to .env and filling in
the paths:
Copy-Item .env.example .envSTEMFLOW_SEPARATOR_EXE=C:\path\to\.sepenv\Scripts\audio-separator.exe
STEMFLOW_FFMPEG_EXE=C:\path\to\ffmpeg\bin\ffmpeg.exe
STEMFLOW_MODEL_DIR=C:\path\to\audio-separator-modelsLeaving a line out is fine when the asset sits somewhere StemFlow already looks — see Asset resolution. Verify with:
.\scripts\preflight.ps1It prints the resolved path and which tier it came from for every component, then confirms CUDA is available. Nothing else runs until it passes.
Requirements
- Windows, an NVIDIA GPU with CUDA, and a CUDA
audio-separatorenvironment (see Prerequisites) - The v4 model assets in
models/— both checkpoints, both.yamlconfigs, anddownload_checks.json(see Models for how to fetch them; they are not in this repository) - Node 22.13+ and Python 3.12
Setup — begins with a CUDA / model / FFmpeg preflight and stops with an explicit missing component if the workstation isn't ready:
.\scripts\setup-local.ps1Run — API and web app in separate terminals:
.\scripts\start-api.ps1 # http://127.0.0.1:8000
.\scripts\start-web.ps1 # http://localhost:3000Health lives at /api/health, interactive docs at
/docs.
Production web build:
.\scripts\build-web.ps1For experiment runs, scripts/batch.py drives separate_job()
directly and skips the HTTP API entirely. Output lands in the normal storage root,
so batched tracks still show up in the studio UI:
.\.venv\Scripts\python.exe scripts\batch.py tracks\
.\.venv\Scripts\python.exe scripts\batch.py tracks\ --overlap 16 --tag overlap16
.\.venv\Scripts\python.exe scripts\batch.py a.wav b.flac --no-autocast --keep-going| Option | Effect |
|---|---|
--overlap N |
Override --mdxc_overlap for this run |
--no-autocast |
Disable mixed precision, for reproducible output |
--tag NAME |
Store a label on each job so runs are tellable apart |
--storage DIR |
Write to a separate storage root |
--keep-going |
Continue past a failed track instead of stopping |
Directories are expanded one level and filtered to supported formats. Batch jobs
are always created with retention disabled, so a run can't be reaped mid-comparison.
Each track prints its job ID and reconstruction error on completion, and the full
original path is kept in sourcePath.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/health |
Separator, encoder, and local model readiness |
GET |
/api/jobs?limit=20 |
Recent jobs |
POST |
/api/jobs |
Upload audio, returns 202 with a job ID |
GET |
/api/jobs/{id} |
Status, staged progress, stem URLs |
GET |
/api/jobs/{id}/preview/{stem} |
MP3 preview for the browser |
GET |
/api/jobs/{id}/stem/{stem} |
Lossless float WAV |
GET |
/api/jobs/{id}/archive |
All stems as a ZIP |
POST |
/api/jobs/{id}/mix |
Render the current mix from { levels } |
DELETE |
/api/jobs/{id} |
Delete a job (rejects while processing) |
Copy .env.example and adjust:
| Variable | Default | Purpose |
|---|---|---|
NEXT_PUBLIC_STEMFLOW_API_BASE |
http://127.0.0.1:8000 |
API base the web app talks to |
STEMFLOW_STORAGE_ROOT |
./storage |
Job storage root |
STEMFLOW_MAX_UPLOAD_BYTES |
524288000 |
Upload cap (500 MB) |
STEMFLOW_RETENTION_HOURS |
24 |
Hours before a job is deleted; 0 keeps jobs forever |
STEMFLOW_WORKER_COUNT |
1 |
Concurrent GPU workers |
STEMFLOW_MDXC_OVERLAP |
8 |
Separator chunk overlap |
STEMFLOW_USE_AUTOCAST |
true |
Mixed-precision inference |
STEMFLOW_ALLOWED_ORIGINS |
localhost origins | CORS allowlist |
STEMFLOW_MODEL_DIR |
<project>/models |
Model directory |
STEMFLOW_OFFLINE |
true |
Block outbound requests from the separator |
STEMFLOW_NODE |
— | Override Node for the web scripts when PATH's is older than 22.13 |
STEMFLOW_SEPARATOR_EXE |
v4 env path | audio-separator executable |
STEMFLOW_FFMPEG_EXE |
v4 env path | FFmpeg executable |
For an internet-facing deployment, run the API and CUDA worker on a GPU host and
build the web app with NEXT_PUBLIC_STEMFLOW_API_BASE pointed at its HTTPS URL.
The hosted build without a GPU still works as a demo.
Experimenting? Set
STEMFLOW_RETENTION_HOURS=0. Retention defaults to 24 hours, which will delete work you left running overnight. Setting0also protects jobs created earlier while retention was enabled — cleanup becomes a no-op rather than only skipping new jobs.
Separation runs with no network access. Inference is local, FFmpeg is local, the API binds to loopback, and the web app pulls no CDN fonts, scripts, or assets. Three things could otherwise have triggered a silent download, and each is closed:
| Risk | Why it existed | How it's closed |
|---|---|---|
| Wrong model directory | audio-separator defaults to /tmp/audio-separator-models/, which on Windows resolves against whichever drive you're on — starting the API from D:\ would look in D:\tmp\…, find nothing, and try to fetch |
--model_file_dir is always passed as an absolute path from STEMFLOW_MODEL_DIR |
Missing .yaml configs |
Without a model's YAML, load_model() falls back to a hash lookup that downloads UVR model data |
Both configs are required assets, checked before separation starts |
Missing download_checks.json |
Fetched from GitHub when absent, even though the checkpoints are already present | Treated as a required asset like the models themselves |
Assets are verified before stage 1, so a missing file fails in milliseconds with an explicit local error instead of stalling on the separator's 300-second download timeout:
Missing 1 model asset(s) in C:\path\to\StemFlow\models: BS-Roformer-SW.yaml.
Separation needs these locally; set STEMFLOW_MODEL_DIR if they live elsewhere.
As a second layer, STEMFLOW_OFFLINE=true (the default) points the separator
subprocess at a dead loopback port, so if a future version tries to fetch
something anyway it fails immediately rather than hanging. Set it to false
only when you actually intend to download a model.
GET /api/health reports the whole picture — modelsReady, modelDir,
missingModelFiles, and offline — and the studio shows demo mode instead of
accepting an upload that could not succeed.
What does need the network: setup-local.ps1 (pip and npm installs, one
time) and the hosted demo site. Neither is involved in local separation.
StemFlow/models/
├── model_bs_roformer_ep_317_sdr_12.9755.ckpt # 639 MB
├── model_bs_roformer_ep_317_sdr_12.9755.yaml # inference params
├── BS-Roformer-SW.ckpt # 699 MB
├── BS-Roformer-SW.yaml # inference params
└── download_checks.json # model index
Verify at any time with:
.\scripts\preflight.ps1Neither model is bundled with this repository — they total about 1.34 GB. Both are BS-Roformer checkpoints distributed through python-audio-separator (MIT), which is also what runs inference.
| Stage | Checkpoint | Known as | By |
|---|---|---|---|
| 1 — vocals | model_bs_roformer_ep_317_sdr_12.9755.ckpt |
Roformer Model: BS-Roformer-Viperx-1297 | viperx |
| 2 — instruments | BS-Roformer-SW.ckpt |
Roformer Model: BS Roformer SW | jarredou |
Fetch both with audio-separator itself, which is the reliable route — it pulls
each checkpoint together with its companion .yaml, and the YAML is as
load-bearing as the weights:
$sep = "C:\path\to\.sepenv\Scripts\audio-separator.exe"
$dir = "$PWD\models"
# audio-separator probes ffmpeg on startup, so it has to be on PATH here.
$env:PATH = "C:\path\to\ffmpeg\bin;$env:PATH"
& $sep --download_model_only --model_filename model_bs_roformer_ep_317_sdr_12.9755.ckpt --model_file_dir $dir
& $sep --download_model_only --model_filename BS-Roformer-SW.ckpt --model_file_dir $dirThe PATH line is only needed for this manual step — the pipeline itself always
injects the FFmpeg directory into the separator's environment.
This is the one step that needs internet, and it also creates the
download_checks.json index. Re-running it when the files already exist is
harmless: it verifies and exits without downloading. After it completes, run
.\scripts\preflight.ps1 to confirm all five assets are in place — separation is
offline from then on.
To browse what else is available: audio-separator --list_models. The upstream
index the tool resolves against is UVR's
download_checks.json.
Swapping models means changing VOCAL_MODEL / SIX_STEM_MODEL in
worker/separate_v4.py. The second model must emit
drums, bass, guitar, and piano for the six-stem layout to hold.
This repository does not contain the separation engine. Inference needs the CUDA
audio-separatorenvironment and the checkpoints above; StemFlow's own dependencies are only FastAPI, NumPy, and SoundFile. The project supplies job handling, alignment, the residual reconstruction, mix rendering, and the studio UI — not the neural network.
Every external tool is resolved in the same order, and what the project owns wins over whatever happens to be installed system-wide so that a checkout behaves the same on any machine:
- The explicit environment variable (
STEMFLOW_MODEL_DIR,STEMFLOW_FFMPEG_EXE,STEMFLOW_SEPARATOR_EXE) - Vendored inside the project — see the layout below
PATH
If none of those turn up a component, resolution fails with an explicit error naming the variable to set — it never falls back to a guess. Drop assets in the project and it stops depending on anything outside itself. All three paths are gitignored:
StemFlow/
├── models/ # the five model assets
├── vendor/ffmpeg/bin/ # ffmpeg.exe (ffprobe.exe optional)
└── .sepenv/ # the CUDA audio-separator venv, if you relocate it
.\scripts\preflight.ps1 prints which tier each component resolved from, so the
remaining external dependencies are always visible:
audio-separator configured C:\path\to\.sepenv\Scripts\audio-separator.exe
ffmpeg bundled C:\path\to\StemFlow\vendor\ffmpeg\bin\ffmpeg.exe
model dir bundled C:\path\to\StemFlow\models
A half-populated models/ never shadows a working install — the directory is
only preferred when all five assets are actually present.
The studio has an optional demo mode that loads six local MP3s so you can try
the mixer without a GPU. No audio ships with this repository — public/demo/
is gitignored, and the demo button only appears when the files are present.
To enable it, put six MP3s of the same length in public/demo/, named
Vocals.mp3, Drums.mp3, Bass.mp3, Guitar.mp3, Piano.mp3, and
Other.mp3. Separating any track you own with scripts/batch.py produces
exactly that set. Only use audio you have the rights to.
Relocating the CUDA venv is not a plain copy. pip's console-script launcher embeds an absolute interpreter path (
#!C:\...\.sepenv\Scripts\python.exe), so a copiedaudio-separator.exestill targets the old location, andcli.pyhas no__main__guard to fall back on. Recreate the venv in place with pip rather than copying it.
storage/jobs/<uuid>/
├── original/source.<ext>
├── stems/{vocals,drums,bass,guitar,piano,other}.wav # float WAV masters
├── preview/{vocals,drums,bass,guitar,piano,other}.mp3 # browser previews
├── exports/stems.zip
├── logs/stage{1-vocals,2-instruments}.log # full separator output
└── metadata.json # atomic writes
Temporary stage directories are removed after processing. Each model pass writes
its complete stdout — including the exact command line and flags — to logs/,
line-buffered so a hung run still leaves something readable behind.
Jobs expire after 24 hours by default, and can be deleted from the studio
immediately. Set STEMFLOW_RETENTION_HOURS=0 to keep them indefinitely.
app/ Next.js studio UI (Web Audio mixer)
server/ FastAPI app, job store, queue manager, mix renderer
worker/ v4 separation worker driving the CUDA audio-separator env
scripts/ PowerShell preflight, setup, start, build
scripts/batch.py Batch runner for experiment sweeps
tests/ Node source-integrity tests
server/tests/ Python store, retention, settings, and invariant tests
npm test # 4 tests
.\.venv\Scripts\python.exe -m unittest discover -s server/tests -t . # 28 testsThe Python suite covers the job store, mix rendering, retention semantics, inference-knob resolution, and — with the separator stubbed out so no GPU is needed — the residual invariant end to end: that the six written stems sum back to the source.
npm test asserts the production path stays honest: real uploads instead of
substituted demo stems, a real transport and export surface in the mixer,
residual reconstruction in the worker, and server-rendered studio output.
StemFlow는 업로드된 오디오를 격리된 UUID 작업으로 만들고, 로컬 CUDA GPU에서 BS-Roformer v4 모델을 2단으로 연달아 실행해 샘플 단위로 정렬된 6개 스템을 Web Audio 믹서에 노출합니다. 믹서에서 솔로·뮤트·밸런스를 조정한 뒤, 무손실 스템이나 새로 렌더링한 24비트 믹스로 내보낼 수 있습니다.
flowchart LR
A[업로드<br/>최대 500 MB] --> B[UUID 작업<br/>metadata.json]
B --> C[GPU 큐<br/>단일 워커]
C --> D[1단계<br/>BS-Roformer ep_317]
D --> E[2단계<br/>BS-Roformer-SW]
E --> F[잔차 복원]
F --> G[Float WAV + MP3<br/>+ ZIP]
G --> H[브라우저 믹서<br/>Web Audio]
H --> I[스템 또는<br/>24비트 믹스 내보내기]
| 🎚️ 6개 스템 | vocals · drums · bass · guitar · piano · other — 모두 샘플 단위 정렬 |
| ⚡ 로컬 GPU | 단일 GPU 백그라운드 큐, 재시작 복구 및 작업 취소 지원 |
| 🔊 브라우저 믹서 | 샘플 동기화 트랜스포트: 구간 이동, 뮤트, 솔로, 마스터 볼륨, 실제 데이터 기반 파형 |
| 💾 무손실 출력 | Float WAV 마스터, 브라우저용 MP3 프리뷰, ZIP 패키징, 서버측 24비트 믹스 렌더링 |
| 📦 작업 격리 | UUID 디렉터리 단위 보존, metadata.json 원자적 기록 |
| 📈 실시간 상태 | 상태 폴링, 단계별 진행률, 명시적 오류, 삭제, 24시간 자동 정리 |
| 🌐 데모 모드 | 믹서를 오프라인으로 둘러보는 선택 기능 — 데모 오디오 참고 |
| 🎧 지원 포맷 | .mp3 .wav .flac .m4a .aac .ogg — 스트리밍 업로드 + 포맷 검증 |
BS-Roformer 모델 2개를 연달아 실행하고, 6번째 스템은 추론하지 않고 계산합니다.
전체 파이프라인은 worker/separate_v4.py에 있으며, 모델
추론 자체는 CUDA audio-separator 실행 파일에 위임합니다. 덕분에 API 프로세스는
NumPy와 SoundFile만 있으면 됩니다.
| # | 단계 | 입력 → 출력 | 진행률 |
|---|---|---|---|
| 0 | 검증 | 원본에 sf.info 실행 — 빈 파일과 모노/스테레오가 아닌 입력을 거부 |
8% |
| 1 | 보컬 분리 — model_bs_roformer_ep_317_sdr_12.9755.ckpt |
source → vocals + instrumental |
12% |
| 2 | 악기 분리 — BS-Roformer-SW.ckpt |
instrumental → drums, bass, guitar, piano |
46% |
| 3 | 정렬 + 잔차 | 5개 스템 + 원본 → 공통 그리드의 float64 배열 6개 | 82% |
| 4 | 마스터 기록 | 원본 샘플레이트 그대로 float32 WAV | 88% |
| 5 | 프리뷰 | 스템별 192 kbps libmp3lame MP3 |
92% |
| 6 | 패키징 | exports/stems.zip |
97% |
2단계는 원본이 아니라 1단계가 만든 반주(instrumental) 를 입력으로 받습니다. 즉 악기 모델이 오디오를 보기 전에 보컬 누출이 이미 제거된 상태입니다.
서로 다른 파일에 대해 모델을 두 번 돌리면 출력 길이가 딱 맞지 않습니다. 이때 리샘플링이나 패딩으로 억지로 맞추지 않고, 6개 신호가 실제로 공유하는 가장 긴 구간을 찾은 뒤 거기에 들어맞지 않는 것은 거부합니다.
length = min(len(mix), *(sf.info(path).frames for path in paths.values()))모든 스템은 float64로 읽어 사용 전에 검증합니다. 샘플레이트 불일치, 채널 수
불일치, 예상보다 짧은 출력은 조용히 보정하지 않고 예외를 냅니다. 늘리거나
리샘플링하거나 0으로 채우는 처리가 전혀 없기 때문에, 브라우저 믹서에서 스템이
샘플 단위로 정확히 맞아떨어집니다.
other는 모델이 예측한 결과가 아닙니다. 악기 모델도 자체 잔여 스템을 내놓지만
StemFlow는 그것을 버리고, 실제 원본 믹스를 기준으로 나머지를 다시 계산합니다.
이 계산을 float64로 수행하므로 다음 불변식이 정확히 성립합니다.
6개 스템을 모두 더하면 정렬된 원본이 된다.
모델 자체 잔여 스템은 "그 모델에 입력된 것"까지만 복원하므로, 이 방식이 더 강한
보장입니다. 대신 트레이드오프가 있고 이를 알고 쓰는 게 좋습니다. other는 진짜
차분이기 때문에 5개 스템이 가져가지 않은 모든 것을 흡수합니다. 모델이 다루지
않는 악기(스트링, 신스, FX), 분리 누출, 그리고 두 모델 단계의 복원 오차까지
포함됩니다. 깔끔한 "그 외 악기" 스템이 아니라 말 그대로 나머지입니다.
마스터는 정규화 없이 float32 WAV로 기록하므로 헤드룸과 스템 간 레벨 관계가
그대로 유지되고, 저장 후에도 잔차 불변식이 계속 유효합니다.
audio-separator는 피크가 1e-6 미만인 스템을 파일로 쓰지 않습니다. 로그와
최종 출력 목록에는 저장했다고 나오지만 파일은 없습니다. 피아노가 없는 곡은
정상적인 입력이므로, 파일이 없는 것은 실패가 아니라 "이 스템은 무음" 으로
해석합니다. 0으로 채워 다른 스템과 동일하게 기록하고, separation.silentStems에
이름을 남깁니다.
무음 스템은 합에 정확히 0을 기여하므로 잔차 불변식이 그대로 성립합니다. 보컬 단계 후 반주가 아예 남지 않는 경우(아카펠라 원본)에는 2단계 자체를 건너뛰고 악기 스템 4개를 모두 0으로 채웁니다.
| 플래그 | 값 | 설정 가능 | 이유 |
|---|---|---|---|
--sample_rate |
원본 레이트 | — | 리샘플링 금지 — 잔차 계산에 공통 그리드가 필요 |
--normalization |
1.0 |
고정 | 레벨을 재조정하면 잔차 불변식이 깨짐 |
--mdxc_overlap |
8 |
STEMFLOW_MDXC_OVERLAP |
청크 오버랩 — 높이면 이음새 품질이 좋아지고 느려짐 |
--use_autocast |
사용 | STEMFLOW_USE_AUTOCAST |
CUDA 혼합 정밀도 추론. 비트 단위 재현이 필요하면 끄기 |
--output_format |
WAV |
— | 단계 간 무손실 전달 |
두 노브는 작업당 한 번만 읽습니다. 실행 중간에 환경변수를 바꿔도 두 모델
단계가 서로 다른 값으로 돌아가는 일이 없습니다. 사용된 값은 metadata.json의
separation 항목에 기록되며, 이것이 같은 곡을 두 번 돌렸을 때 비교가 성립하는
근거입니다.
"separation": {
"vocalModel": "model_bs_roformer_ep_317_sdr_12.9755.ckpt",
"instrumentModel": "BS-Roformer-SW.ckpt",
"mdxcOverlap": 8,
"autocast": true,
"reconstructionError": 1.19e-07,
"silentStems": ["piano"]
}reconstructionError는 |스템 합 - 원본|의 피크값이며, 메모리 상태가 아니라
디스크에 기록된 float32 데이터를 기준으로 측정합니다. float32 변환 때문에
1e-7 정도가 정상이고, 이보다 뚜렷하게 크면 어딘가에서 불변식이 깨진 것입니다.
서버측 내보내기(server/mix.py)는 각 페이더 값을 float64로
적용해 합산한 뒤 24비트 PCM으로 기록합니다. 레벨은 0 ≤ v ≤ 1.5 범위로 검증하고,
피크 정규화는 합이 실제로 풀스케일을 넘을 때만 동작합니다. 따라서 범위에 들어가는
믹스는 스템과 비트 단위로 일치합니다.
if peak > 1:
mixed /= peak취소는 협조적(cooperative) 방식으로, 의미 있는 모든 경계에서 확인합니다. 단계
사이, 분리기 출력 읽기 루프 내부(terminate → 10초 대기 → kill), 그리고 스템
기록과 프리뷰 인코딩 직전마다 검사합니다. 임시 디렉터리 _stage1 / _stage2는
finally 블록에서 제거되므로, 취소되거나 실패한 작업이 부분 데이터를 남기지
않습니다. 진행률과 단계 문자열은 임시 파일 replace를 통해 metadata.json에
원자적으로 기록되며, 이것이 재시작 후에도 큐가 상태를 복구할 수 있는 근거입니다.
Important
StemFlow는 분리 엔진과 모델 가중치를 포함하지 않습니다. 이것들은 직접
설치하고 경로를 알려주셔야 합니다. 없으면 스튜디오는 실행되지만 데모 모드로만
동작합니다 — /api/health가 modelsReady: false를 보고하고, 업로드를 받아놓고
실패하는 대신 아예 거부합니다.
CUDA를 지원하는 NVIDIA GPU가 필요합니다. preflight.ps1이
torch.cuda.is_available()을 검사하고 실패하면 중단합니다 — 이 프로젝트의 설치
흐름에 CPU 경로는 없으며, 이 모델들을 CPU로 돌리면 대화형으로 쓰기엔 너무
느립니다.
기준 환경에서 측정한 값입니다.
| GPU | NVIDIA RTX 5060 Ti, VRAM 15.9 GB, compute capability sm_120 |
| 스택 | PyTorch 2.11.0+cu128, CUDA 12.8, cuDNN 9.19 |
| 분리 중 VRAM 피크 | 전체 5.8 GB, 이 중 약 2.7 GB가 분리 몫 (유휴 상태에서 이미 3.0 GB 사용 중) |
| 속도 (12초 스테레오 44.1 kHz) | 단계당 추론 약 2초(합 약 4초), 체크포인트 로딩 각 1~2초, API 종단 간 약 21초 |
작업마다 체크포인트 2개를 모두 올리므로 가용 VRAM 4 GB 정도가 현실적인 하한이고 6~8 GB면 여유롭습니다.
21초 중 실제 추론은 약 4초뿐입니다. 나머지는 작업당 고정 오버헤드입니다 —
audio-separator 프로세스 2개가 각각 PyTorch를 import하고 약 700 MB 체크포인트를
로드하며, 이후 MP3 6개 인코딩과 ZIP 생성이 이어집니다. 추론 시간은 곡 길이에
비례하지만 오버헤드는 그렇지 않으므로, 짧은 클립에서는 오버헤드가 지배적이고 긴
곡에서는 비중이 훨씬 작아집니다. 여러 곡을 배치로 돌릴 때 GPU 시간만으로
예상한 것보다 느린 이유도 이것입니다.
Warning
PyTorch 빌드를 GPU에 맞춰야 합니다. 그냥 pip install torch를 하면 내 GPU
아키텍처를 포함하지 않는 CUDA 빌드가 깔릴 수 있고, 그러면 import는 되지만 GPU를
못 쓰는 환경이 만들어집니다. 이 PC의 sm_120(Blackwell)은 CUDA 12.8 이상이
필요합니다. 확인 방법:
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_arch_list())"
이 저장소에 없는 세 가지가 필요합니다.
| # | 항목 | 크기 | 방법 |
|---|---|---|---|
| 1 | CUDA audio-separator 환경 (PyTorch + CUDA) |
약 4.8 GB | 별도 venv에 pip install "audio-separator[gpu]", GPU에 맞는 PyTorch 빌드 필요 |
| 2 | FFmpeg | 약 200 MB | 최신 빌드 아무거나 |
| 3 | BS-Roformer 체크포인트 2개 + .yaml 설정 |
약 1.34 GB | 모델 참고 |
디스크 약 6.5 GB와 그에 준하는 최초 1회 다운로드가 필요합니다. 새 PC에서는
pip install 한 번으로 끝나는 수준이 아니라 실제 세팅 작업이며, CUDA 환경이
가장 손이 많이 가는 부분입니다.
그 다음 .env.example을 .env로 복사해 경로를 채워 StemFlow에 알려줍니다.
Copy-Item .env.example .envSTEMFLOW_SEPARATOR_EXE=C:\path\to\.sepenv\Scripts\audio-separator.exe
STEMFLOW_FFMPEG_EXE=C:\path\to\ffmpeg\bin\ffmpeg.exe
STEMFLOW_MODEL_DIR=C:\path\to\audio-separator-modelsStemFlow가 이미 찾아보는 위치에 에셋이 있다면 해당 줄은 비워둬도 됩니다 — 에셋 탐색 순서를 참고하세요. 확인은:
.\scripts\preflight.ps1각 구성요소의 해석된 경로와 어느 단계에서 찾았는지를 출력한 뒤 CUDA 가용성을 확인합니다. 이걸 통과하지 못하면 아무것도 실행되지 않습니다.
요구 사항
- Windows, CUDA 지원 NVIDIA GPU, 그리고 CUDA
audio-separator환경 (사전 준비 참고) models/에 v4 모델 에셋 — 체크포인트 2개,.yaml설정 2개,download_checks.json(받는 방법은 모델 참고. 저장소에 포함되어 있지 않습니다)- Node 22.13 이상, Python 3.12
설치 — CUDA / 모델 / FFmpeg 사전 점검부터 시작하며, 준비되지 않은 항목이 있으면 무엇이 없는지 명시하고 중단합니다.
.\scripts\setup-local.ps1실행 — API와 웹 앱을 각각 다른 터미널에서 띄웁니다.
.\scripts\start-api.ps1 # http://127.0.0.1:8000
.\scripts\start-web.ps1 # http://localhost:3000헬스 체크는 /api/health, 대화형 문서는
/docs에 있습니다.
프로덕션 웹 빌드:
.\scripts\build-web.ps1여러 곡을 돌려보고 비교할 때는 scripts/batch.py가 HTTP API를
거치지 않고 separate_job()을 직접 호출합니다. 결과물은 평소와 같은 저장 루트에
들어가므로, 배치로 처리한 곡도 스튜디오 UI에서 그대로 볼 수 있습니다.
.\.venv\Scripts\python.exe scripts\batch.py tracks\
.\.venv\Scripts\python.exe scripts\batch.py tracks\ --overlap 16 --tag overlap16
.\.venv\Scripts\python.exe scripts\batch.py a.wav b.flac --no-autocast --keep-going| 옵션 | 효과 |
|---|---|
--overlap N |
이번 실행의 --mdxc_overlap 값을 덮어씀 |
--no-autocast |
혼합 정밀도를 끔 — 재현 가능한 출력용 |
--tag NAME |
각 작업에 라벨을 저장해 실행을 구분 |
--storage DIR |
별도 저장 루트에 기록 |
--keep-going |
실패한 곡이 있어도 중단하지 않고 계속 |
디렉터리는 한 단계까지 펼쳐지고 지원 포맷만 걸러냅니다. 배치 작업은 항상 보관
기간을 끈 상태로 생성되므로 비교하는 중간에 결과가 삭제되지 않습니다. 각 곡이
끝날 때 작업 ID와 재구성 오차를 출력하고, 원본 전체 경로는 sourcePath에
보존됩니다.
| 메서드 | 엔드포인트 | 설명 |
|---|---|---|
GET |
/api/health |
분리기·인코더·로컬 모델 준비 상태 |
GET |
/api/jobs?limit=20 |
최근 작업 목록 |
POST |
/api/jobs |
오디오 업로드, 작업 ID와 함께 202 반환 |
GET |
/api/jobs/{id} |
상태, 단계별 진행률, 스템 URL |
GET |
/api/jobs/{id}/preview/{stem} |
브라우저용 MP3 프리뷰 |
GET |
/api/jobs/{id}/stem/{stem} |
무손실 float WAV |
GET |
/api/jobs/{id}/archive |
전체 스템 ZIP |
POST |
/api/jobs/{id}/mix |
{ levels }로 현재 믹스 렌더링 |
DELETE |
/api/jobs/{id} |
작업 삭제 (처리 중이면 거부) |
.env.example을 복사해 값을 조정합니다.
| 변수 | 기본값 | 용도 |
|---|---|---|
NEXT_PUBLIC_STEMFLOW_API_BASE |
http://127.0.0.1:8000 |
웹 앱이 호출할 API 주소 |
STEMFLOW_STORAGE_ROOT |
./storage |
작업 저장 루트 |
STEMFLOW_MAX_UPLOAD_BYTES |
524288000 |
업로드 상한 (500 MB) |
STEMFLOW_RETENTION_HOURS |
24 |
작업 삭제까지의 시간. 0이면 영구 보관 |
STEMFLOW_WORKER_COUNT |
1 |
동시 GPU 워커 수 |
STEMFLOW_MDXC_OVERLAP |
8 |
분리기 청크 오버랩 |
STEMFLOW_USE_AUTOCAST |
true |
혼합 정밀도 추론 |
STEMFLOW_ALLOWED_ORIGINS |
localhost 주소들 | CORS 허용 목록 |
STEMFLOW_MODEL_DIR |
<project>/models |
모델 디렉터리 |
STEMFLOW_OFFLINE |
true |
분리기의 외부 요청 차단 |
STEMFLOW_NODE |
— | PATH의 Node가 22.13 미만일 때 웹 스크립트용 Node 지정 |
STEMFLOW_SEPARATOR_EXE |
v4 환경 경로 | audio-separator 실행 파일 |
STEMFLOW_FFMPEG_EXE |
v4 환경 경로 | FFmpeg 실행 파일 |
외부에 공개하려면 API와 CUDA 워커를 GPU 호스트에서 실행하고, 웹 앱은
NEXT_PUBLIC_STEMFLOW_API_BASE를 그 호스트의 HTTPS 주소로 지정해 빌드하세요.
GPU 없이 호스팅한 빌드도 데모로는 그대로 동작합니다.
이것저것 실험할 거라면
STEMFLOW_RETENTION_HOURS=0으로 두세요. 기본값이 24시간이라 밤새 돌려둔 작업이 지워집니다.0은 보관 기간이 켜져 있던 동안 만들어진 기존 작업까지 보호합니다 — 새 작업만 건너뛰는 게 아니라 정리 자체가 동작하지 않습니다.
분리는 네트워크 없이 동작합니다. 추론도 FFmpeg도 로컬이고, API는 루프백에 바인딩되며, 웹 앱은 CDN 폰트·스크립트·에셋을 전혀 불러오지 않습니다. 조용히 다운로드가 발생할 수 있던 지점이 세 곳 있었고, 각각 막았습니다.
| 위험 | 왜 존재했나 | 어떻게 막았나 |
|---|---|---|
| 잘못된 모델 디렉터리 | audio-separator의 기본값 /tmp/audio-separator-models/는 Windows에서 현재 드라이브 기준으로 해석됩니다. D:\에서 API를 띄우면 D:\tmp\…를 찾고 → 없으니 다운로드 시도 |
STEMFLOW_MODEL_DIR의 절대 경로를 --model_file_dir로 항상 명시 |
.yaml 설정 누락 |
YAML이 없으면 load_model()이 해시 조회로 빠지면서 UVR 모델 데이터를 내려받음 |
두 설정 파일을 필수 에셋으로 지정하고 분리 시작 전에 검사 |
download_checks.json 누락 |
체크포인트가 이미 있어도 이 파일이 없으면 GitHub에서 가져옴 | 모델과 동일하게 필수 에셋으로 취급 |
에셋 검사는 1단계 이전에 실행되므로, 파일이 없으면 분리기의 300초 다운로드 타임아웃에 걸리는 대신 밀리초 단위로 명확한 로컬 오류가 납니다.
Missing 1 model asset(s) in C:\path\to\StemFlow\models: BS-Roformer-SW.yaml.
Separation needs these locally; set STEMFLOW_MODEL_DIR if they live elsewhere.
두 번째 방어선으로 STEMFLOW_OFFLINE=true(기본값)가 분리기 서브프로세스를 죽은
루프백 포트로 향하게 합니다. 향후 버전이 무언가를 가져오려 해도 멈추지 않고 즉시
실패합니다. 실제로 모델을 내려받아야 할 때만 false로 두세요.
GET /api/health가 modelsReady, modelDir, missingModelFiles, offline로
전체 상태를 알려주며, 스튜디오는 성공할 수 없는 업로드를 받는 대신 데모 모드를
표시합니다.
네트워크가 필요한 것: setup-local.ps1의 pip·npm 설치(최초 1회)와 호스팅된
데모 사이트. 둘 다 로컬 분리와는 무관합니다.
StemFlow/models/
├── model_bs_roformer_ep_317_sdr_12.9755.ckpt # 639 MB
├── model_bs_roformer_ep_317_sdr_12.9755.yaml # 추론 파라미터
├── BS-Roformer-SW.ckpt # 699 MB
├── BS-Roformer-SW.yaml # 추론 파라미터
└── download_checks.json # 모델 인덱스
언제든 다음으로 확인할 수 있습니다:
.\scripts\preflight.ps1두 모델 모두 이 저장소에 포함되어 있지 않습니다 — 합쳐서 약 1.34 GB입니다. 둘 다 python-audio-separator(MIT)를 통해 배포되는 BS-Roformer 체크포인트이며, 추론도 이 도구가 담당합니다.
| 단계 | 체크포인트 | 알려진 이름 | 제작 |
|---|---|---|---|
| 1 — 보컬 | model_bs_roformer_ep_317_sdr_12.9755.ckpt |
Roformer Model: BS-Roformer-Viperx-1297 | viperx |
| 2 — 악기 | BS-Roformer-SW.ckpt |
Roformer Model: BS Roformer SW | jarredou |
audio-separator로 직접 받는 것이 가장 확실합니다. 체크포인트와 짝이 되는
.yaml을 함께 받아오는데, 이 YAML은 가중치만큼이나 중요합니다:
$sep = "C:\path\to\.sepenv\Scripts\audio-separator.exe"
$dir = "$PWD\models"
# audio-separator가 시작 시 ffmpeg를 찾으므로 PATH에 있어야 합니다.
$env:PATH = "C:\path\to\ffmpeg\bin;$env:PATH"
& $sep --download_model_only --model_filename model_bs_roformer_ep_317_sdr_12.9755.ckpt --model_file_dir $dir
& $sep --download_model_only --model_filename BS-Roformer-SW.ckpt --model_file_dir $dirPATH 설정은 이 수동 단계에만 필요합니다 — 파이프라인은 분리기 환경에 FFmpeg
디렉터리를 항상 직접 주입합니다.
여기가 인터넷이 필요한 유일한 단계이고, 동시에 download_checks.json 인덱스도
생성됩니다. 파일이 이미 있는 상태에서 다시 실행해도 무해합니다 — 확인만 하고
다운로드 없이 종료합니다. 끝난 뒤 .\scripts\preflight.ps1로 5개 에셋이 모두
있는지 확인하면, 그 이후 분리는 완전히 오프라인입니다.
다른 모델 목록을 보려면 audio-separator --list_models. 이 도구가 참조하는 상위
인덱스는 UVR의
download_checks.json입니다.
모델을 바꾸려면 worker/separate_v4.py의 VOCAL_MODEL /
SIX_STEM_MODEL을 수정하면 됩니다. 단, 6스템 구조가 성립하려면 두 번째 모델이
drums, bass, guitar, piano를 출력해야 합니다.
이 저장소에는 분리 엔진이 들어있지 않습니다. 추론에는 CUDA
audio-separator환경과 위 체크포인트가 필요하고, StemFlow 자체 의존성은 FastAPI·NumPy·SoundFile뿐입니다. 이 프로젝트가 제공하는 것은 작업 관리, 정렬, 잔차 복원, 믹스 렌더링, 스튜디오 UI이며 — 신경망 자체는 아닙니다.
모든 외부 도구를 같은 순서로 찾으며, 프로젝트가 소유한 것이 시스템 전역에 설치된 것보다 우선합니다. 어느 PC에서든 같은 동작을 보장하기 위한 것입니다.
- 명시적 환경변수 (
STEMFLOW_MODEL_DIR,STEMFLOW_FFMPEG_EXE,STEMFLOW_SEPARATOR_EXE) - 프로젝트 내부 — 아래 레이아웃 참고
PATH
이 중 어디에서도 못 찾으면 설정해야 할 변수 이름을 명시한 오류로 실패합니다 — 임의로 추측해 넘어가지 않습니다. 아래 위치에 에셋을 두면 프로젝트가 외부에 의존하지 않게 되며, 세 경로 모두 gitignore 처리되어 있습니다.
StemFlow/
├── models/ # 모델 에셋 5개
├── vendor/ffmpeg/bin/ # ffmpeg.exe (ffprobe.exe는 선택)
└── .sepenv/ # CUDA audio-separator venv를 옮겨올 경우
.\scripts\preflight.ps1이 각 구성요소를 어느 단계에서 찾았는지 출력하므로,
남아있는 외부 의존을 항상 눈으로 확인할 수 있습니다.
audio-separator configured C:\path\to\.sepenv\Scripts\audio-separator.exe
ffmpeg bundled C:\path\to\StemFlow\vendor\ffmpeg\bin\ffmpeg.exe
model dir bundled C:\path\to\StemFlow\models
일부만 채워진 models/가 정상 설치를 가리는 일은 없습니다 — 5개 에셋이 모두
있을 때만 해당 디렉터리를 우선합니다.
스튜디오에는 GPU 없이 믹서를 체험할 수 있도록 로컬 MP3 6개를 불러오는 선택적
데모 모드가 있습니다. 이 저장소에는 오디오가 포함되지 않습니다 —
public/demo/는 gitignore 처리되어 있고, 파일이 있을 때만 데모 버튼이 나타납니다.
사용하려면 길이가 같은 MP3 6개를 public/demo/에 Vocals.mp3, Drums.mp3,
Bass.mp3, Guitar.mp3, Piano.mp3, Other.mp3 이름으로 넣으면 됩니다.
본인이 권리를 가진 곡을 scripts/batch.py로 분리하면 정확히 그 구성이 나옵니다.
권리가 있는 오디오만 사용하세요.
CUDA venv는 단순 복사로 옮길 수 없습니다. pip의 콘솔 스크립트 런처가 절대 인터프리터 경로(
#!C:\...\.sepenv\Scripts\python.exe)를 내장하고 있어서, 복사한audio-separator.exe는 여전히 원래 위치를 가리킵니다.cli.py에__main__가드도 없어python -m으로 우회할 수도 없습니다. 복사하지 말고 pip으로 새로 만들어야 합니다.
storage/jobs/<uuid>/
├── original/source.<ext>
├── stems/{vocals,drums,bass,guitar,piano,other}.wav # float WAV 마스터
├── preview/{vocals,drums,bass,guitar,piano,other}.mp3 # 브라우저 프리뷰
├── exports/stems.zip
├── logs/stage{1-vocals,2-instruments}.log # 분리기 전체 출력
└── metadata.json # 원자적 기록
임시 작업 디렉터리는 처리가 끝나면 삭제됩니다. 각 모델 단계는 실행된 명령줄과
플래그를 포함한 stdout 전체를 logs/에 기록하며, 라인 버퍼링이라 실행이 멈춰도
읽을 수 있는 로그가 남습니다.
작업은 기본 24시간 후 만료되며 스튜디오에서 즉시 삭제할 수도 있습니다.
STEMFLOW_RETENTION_HOURS=0으로 두면 영구 보관됩니다.
app/ Next.js 스튜디오 UI (Web Audio 믹서)
server/ FastAPI 앱, 작업 저장소, 큐 매니저, 믹스 렌더러
worker/ CUDA audio-separator 환경을 구동하는 v4 분리 워커
scripts/ PowerShell 사전 점검, 설치, 실행, 빌드
scripts/batch.py 파라미터 비교 실험용 배치 러너
tests/ Node 소스 정합성 테스트
server/tests/ Python 저장소·보관 기간·설정·불변식 테스트
npm test # 4개
.\.venv\Scripts\python.exe -m unittest discover -s server/tests -t . # 28개Python 스위트는 작업 저장소, 믹스 렌더링, 보관 기간 동작, 추론 노브 해석을 검사하고 — 분리기를 스텁으로 대체해 GPU 없이 — 잔차 불변식을 종단 간으로 확인합니다. 즉 기록된 6개 스템을 더하면 원본이 되는지를 실제로 검증합니다.
npm test는 프로덕션 경로가 변질되지 않았는지 검사합니다. 데모 스템으로
대체하지 않고 실제 업로드를 쓰는지, 믹서에 실제 트랜스포트와 내보내기 경로가
있는지, 워커가 잔차 복원을 유지하는지, 스튜디오가 서버에서 렌더링되는지를
확인합니다.