Commit 607c408d by PLN (Algolia)

feat(tide-table): foundry_stems — bridge to the Foundry demucs engine

Stem-aware emotion needs the voice isolated: the full mix is muddy for CLAP, but
the VOCALS carry valence, DRUMS+BASS carry arousal, and vocal presence is itself a
section/structure cue. ensure_stems(file, slug) consumes the Foundry's in-process
Python API (tools/foundry, the shared separation engine) → {mix,vocals,bass,drums,
other} absolute paths, idempotent via catch.json (re-runs free). Validated: 4-stem
demucs separation of a 6-min track (Bohemian Rhapsody) lands all four stems clean.
Feeds emotion_timeline's stem-aware analysis (#89) and the hexa lane (#90).
parent ecfa108c
#!/usr/bin/env python3
"""foundry_stems — thin bridge to the Foundry demucs engine for stem-aware emotion.
The Foundry (tools/foundry) is the shared stem-separation engine; we consume its
in-process Python API to turn a track into vocals/bass/drums/other, idempotently
(catch.json caches the result, so re-runs are free). Why stems: the full mix is
muddy for CLAP — the VOCALS carry valence, DRUMS+BASS carry arousal, and vocal
presence is itself a section/structure cue. See ../../tools/foundry/README.md
("Use as an API") and [[project_foundry]] / [[project_hexa_emotion_convergence]].
python3 foundry_stems.py <audio-file> [slug] # separate one file, print stem paths
"""
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
FOUNDRY = HERE.parent.parent / "tools" / "foundry"
WS = HERE / "_corpus_cache" / "foundry"
STEM_NAMES = ("vocals", "bass", "drums", "other")
def ensure_stems(audio_path, slug, *, shifts=1, log=print):
"""Local audio file → {mix, vocals, bass, drums, other} → absolute Paths.
Reuses existing stems if the catch is already separated (idempotent)."""
import shutil
if str(FOUNDRY) not in sys.path:
sys.path.insert(0, str(FOUNDRY))
from engine import separate
from engine.model import Catch
WS.mkdir(parents=True, exist_ok=True)
d = WS / slug
if (d / "catch.json").exists():
c = Catch.load(d)
else:
d.mkdir(parents=True, exist_ok=True)
src = d / ("source" + Path(audio_path).suffix.lower())
if not src.exists():
shutil.copy(audio_path, src)
c = Catch(slug=slug, title=slug, source_path=src.name)
c.save(WS)
if not c.separated:
c = separate.separate(c, WS, backend="demucs", shifts=shifts, log=log)
c.save(WS)
cd = c.dir(WS)
out = {"mix": cd / c.source_path}
for s in c.stems:
out[s.name] = cd / s.path
return out
def main():
if len(sys.argv) < 2:
sys.exit("usage: foundry_stems.py <audio-file> [slug]")
p = Path(sys.argv[1])
slug = sys.argv[2] if len(sys.argv) > 2 else p.stem
stems = ensure_stems(p, slug)
for name, path in stems.items():
print(f" {name:<7} {path}")
if __name__ == "__main__":
main()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment