# ⚓ The Foundry — ParVagues sample foundry

**URL → audio → stems → (soon) loops.** Turns a track URL into separated stems
ready for sampling. One engine, two faces: a CLI (`foundry.py`) and a web GUI
(`foundry.py serve`) that both call the same `engine/` steps.

Replaces the manual `yt-dlp → activate demucs venv → demucs --shifts 4 → open
4 stems in Audacity → find loops` dance. engine1 ships the **download + stem**
half today; the loop-finder (the Audacity-replacement) is phase 2.

## Quickstart

```bash
cd tools/foundry
python3 foundry.py backends                          # what separation engines are live
python3 foundry.py catch 'https://youtu.be/…'        # full pipeline: fetch + separate
python3 foundry.py serve                             # GUI at http://127.0.0.1:8765/
```

Workspace defaults to `$FOUNDRY_HOME` or `~/Downloads/foundry`. Each catch is a
self-describing folder — the filesystem *is* the database:

```
~/Downloads/foundry/<slug>/
  source.webm            # downloaded bestaudio
  source.info.json       # yt-dlp metadata sidecar (provenance)
  stems/{drums,bass,other,vocals}.wav
  catch.json             # the Catch model (title, url, backend/model, stems…)
```

## CLI

```bash
foundry.py fetch <url>        # download bestaudio → a Catch (no separation)
foundry.py sep <slug|url>     # separate an existing catch (or fetch+sep a url)
foundry.py catch <url>        # fetch + sep (the whole pipeline)
foundry.py list               # ▣ separated / ▢ raw, per catch
foundry.py backends           # ✓/✗ + hint per backend
foundry.py serve [--port]     # launch the GUI

# separation knobs (sep / catch):
  --backend demucs            # which engine (demucs today, roformer next)
  --model htdemucs            # backend model
  --shifts 4                  # demucs equivariant shifts (quality↔time)
  --jobs N  --device cuda|cpu
```

Runs under **system python3** (pydantic). Heavy work shells out to dedicated
venvs (demucs at `~/.virtualenvs/demucs/bin/demucs`; override `$FOUNDRY_DEMUCS`).

## Tests

```bash
cd tools/foundry && python3 -m pytest tests/ -q   # pure engine: slug, parse, argv, registry
```

---

## Engine roadmap — engine1 → engine2

The separation backend is a **registry** (`engine/separate.py`); the CLI/GUI ask
for "a backend" and never hardcode demucs. That's the whole point.

### engine1 — Demucs (htdemucs) · LIVE
Meta's Demucs v4, the model PLN already runs by hand. Fits our **6 GB RTX 2060
Max-Q** comfortably. Reliable baseline for "grab a usable stem". Knobs: `--shifts`
(more = cleaner separation via test-time augmentation, linearly slower; 1 for
scratch, 4–10 for keeps), `--model` (`htdemucs` default, `htdemucs_ft` = the
fine-tuned 4-model bag, ~4× slower, a touch cleaner).

### engine2 — BS-/Mel-Band Roformer (SOTA) · STUB
Frequency-domain transformers (ByteDance) now top the MVSEP leaderboard —
**~+3 SDR on vocals** (≈11.9 vs htdemucs 8.8), ~9.76 SDR on MUSDB18HQ, better
transients + low-end (exactly what matters for carving clean bass/drum loops).
Wire via [`python-audio-separator`](https://github.com/nomadkaraoke/python-audio-separator)
(wraps the UVR Roformer ckpts) in its own venv; register a working
`RoformerBackend`.

**Hyperparams to expose** (audio-separator / MDXC):
| knob | default | trade-off |
|------|---------|-----------|
| `segment_size` | 256–512 | larger = more context, better quality, **more VRAM** |
| `overlap` | 8 (range 2–50) | higher = fewer chunk-seam artifacts, slower |
| `batch_size` | 1 | higher = faster, more RAM |
| `chunk_size` (infer) | 352800 (8 s) | the value models trained on; rarely change |
| model ckpt | `bs_roformer_…sdr_12.97` | pick per stem target (vocal vs 4-stem) |

**The 6 GB constraint is the open question:** Roformer is VRAM-hungry. Before
making it a default, run a **VRAM spike** — find the largest `segment_size` that
fits 6 GB (fall back to `--device cpu` or smaller segments / lower batch).
Sources: [MVSEP leaderboard](https://mvsep.com/en/algorithms),
[Mel-Band RoFormer paper](https://arxiv.org/html/2310.01809v1),
[audio-separator](https://pypi.org/project/audio-separator/).

---

## Phase 2 — the loop finder (Audacity-replacement)

Charter for a dedicated deep-research pass (Sonnet 4.6 agents): study the
ParVagues `_samples` folder + sampling fundamentals, and design an **easy**
loop-detection engine that automates the mechanical drudgery while **leaving the
creative loop-choice to PLN's ear**. Lands as `engine/loops.py` + a GUI panel.

Automate (machine): tempo/beat/downbeat grid (librosa), bar-aligned candidate
regions, **zero-crossing snap** for click-free loop points, energy/novelty to
rank candidates, one-shot vs loop classification. Leave to the human: *which*
loop is musical, naming, the creative cut.

## UX roadmap (asked-for surface: save / settings / analyse / review / folders)

- **Catch review** — the card list is the review surface; per-stem `<audio>`
  (Range-seekable). Phase 2 adds waveforms (wavesurfer.js) + loop regions.
- **Save / export** — today: `⬇ wav` per stem. Phase 2: "send loop → sample
  folder" with a Tidal-friendly name into **`~/Work/Sound/Samples/<kit>/`** (the
  real custom SuperDirt sample root — where crimewave/diams_dj/humpty already
  live), then `foundry.py link <kit>` symlinks it into Dirt-Samples so it loads
  as `s "<kit>"`. The link step is live today (`engine/publish.py`).
- **Settings** — engine + shifts live in the GUI opts bar; later: persisted
  defaults (engine, output sample-rate, target folder) in a `foundry.config.json`.
- **Analyse** — reuse `tools/analyze_samples.py` / tide-table `audio_lens.py` for
  EDA (centroid, fundamental, band energy) on each stem before sampling — the
  required mastering-EDA step, surfaced inline so role is *validated, not guessed*.
- **Folders** — workspace per-catch dirs (above); a "catches" library view with
  search/filter as the corpus grows.
