Instrumente und Stimmen trennen
Wähle zwischen der besonders sauberen Gesangstrennung und zusammengehörigen Mehrspur-Modellen.
- -From 2342495d24f232ca82ae0ce84581439e89ccdf10 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Wed, 9 Sep 2026 01:00:01 +0200 Subject: [PATCH] feat: add target-and-remainder stem extraction --- docs/OPERATING_MODES.md | 13 ++-- .../bs-roformer-vocal-separation/README.md | 19 +++--- .../bs-roformer-vocal-separation/app.py | 61 ++++++++++++++++--- .../bs-roformer-vocal-separation/index.html | 18 ++++-- 4 files changed, 85 insertions(+), 26 deletions(-) diff --git a/docs/OPERATING_MODES.md b/docs/OPERATING_MODES.md index 337dca5..2567973 100644 --- a/docs/OPERATING_MODES.md +++ b/docs/OPERATING_MODES.md @@ -36,12 +36,13 @@ Docker-Netz `mike-ai-music` auf `http://music-worker:7860` zu. Im Trennmodus öffnet das Dashboard die private Athena-Oberfläche unter `http://192.168.1.212:8007`. Sie nimmt WAV, FLAC, MP3, M4A und weitere -übliche Formate an und liefert verlustfreie FLAC-Spuren als ZIP. Verfügbar sind -die spezialisierte Gesangstrennung (`vocals`, `instrumental`), vier Spuren -(`vocals`, `drums`, `bass`, `other`) und experimentell sechs Spuren -(`vocals`, `drums`, `bass`, `guitar`, `piano`, `other`). Grundlage ist -`audio-separator` 0.47.0 mit BS-RoFormer Viperx 1297, `htdemucs_ft` und -`htdemucs_6s`. +übliche Formate an. Gewählt wird die herauszulösende Quelle: Gesang, +Schlagzeug, Bass, Gitarre oder Piano. Das ZIP enthält genau diese Zielspur und +eine zweite FLAC-Datei mit dem vollständigen Rest ohne die Zielspur. Gesang +nutzt BS-RoFormer Viperx 1297, Schlagzeug/Bass `htdemucs_ft` und +Gitarre/Piano experimentell `htdemucs_6s`. Grundlage ist `audio-separator` +0.47.0. Die ältere API-Auswahl kompletter 2-/4-/6-Stem-Sätze bleibt +rückwärtskompatibel. Hermes benötigt dafür kein Plugin. Exakt eingegebene Steuerbefehle werden vom Router lokal beantwortet, auch wenn gerade kein LLM geladen ist: diff --git a/experiments/bs-roformer-vocal-separation/README.md b/experiments/bs-roformer-vocal-separation/README.md index 10036b5..0a8d4a0 100644 --- a/experiments/bs-roformer-vocal-separation/README.md +++ b/experiments/bs-roformer-vocal-separation/README.md @@ -1,15 +1,18 @@ # Athena Stem Separator -Exklusiver dritter Athena-Betriebsmodus für lokale Zwei- bis Sechs-Spur-Trennung. +Exklusiver dritter Athena-Betriebsmodus zum gezielten Herauslösen einer Quelle. +Der Download enthält immer die Zielspur und eine zweite Spur mit dem kompletten +Rest ohne dieses Ziel. - Engine: `audio-separator` 0.47.0 (MIT) - **Gesang / Instrumental:** BS-RoFormer Viperx 1297, `model_bs_roformer_ep_317_sdr_12.9755.ckpt`; Vocal SDR 12,9, Instrumental SDR 17,0. -- **4 Spuren:** `htdemucs_ft.yaml` mit Gesang, Schlagzeug, Bass und Rest. -- **6 Spuren (experimentell):** `htdemucs_6s.yaml` ergänzt gemeinsame Gitarre - und Piano. Die Instrumentqualität liegt unter der spezialisierten - Zwei-Spur-Trennung. +- **Schlagzeug oder Bass:** `htdemucs_ft.yaml`; die nicht gewählten Stems werden + zu einer gemeinsamen Restspur summiert. +- **Gitarre oder Piano (experimentell):** `htdemucs_6s.yaml`; auch hier werden + alle übrigen Stems wieder zur Restspur zusammengesetzt. Die Instrumentqualität + liegt unter der spezialisierten Gesangstrennung. - GPU: RTX 5080; LLM, Bildmodelle, TTS und ACE-Step sind dabei verriegelt. - Privat erreichbar: `http://192.168.1.212:8007/` @@ -19,5 +22,7 @@ werden nach dem ZIP-Download entfernt. Eigene Spuren für E-/Akustikgitarre, Synthesizer und Streicher sind bewusst noch nicht angeboten: Dafür braucht es weitere Zielmodelle; sie aus `other` umzubenennen wäre fachlich falsch. -Die API erwartet `multipart/form-data` mit `file` und optional `mode`: -`vocals` (Standard), `four_stem` oder `six_stem`. +Die API erwartet `multipart/form-data` mit `file` und optional `target`: +`vocals` (Standard), `drums`, `bass`, `guitar` oder `piano`. Das ältere Feld +`mode` mit `vocals`, `four_stem` oder `six_stem` bleibt für vorhandene Clients +erhalten und liefert weiterhin alle Modell-Stems. diff --git a/experiments/bs-roformer-vocal-separation/app.py b/experiments/bs-roformer-vocal-separation/app.py index 2ae6e0a..ba9ff76 100644 --- a/experiments/bs-roformer-vocal-separation/app.py +++ b/experiments/bs-roformer-vocal-separation/app.py @@ -27,6 +27,13 @@ MODES = { "four_stem": {"model": "htdemucs_ft.yaml", "stems": ("vocals", "drums", "bass", "other"), "archive": "athena-4-stems.zip", "engine": "demucs"}, "six_stem": {"model": "htdemucs_6s.yaml", "stems": ("vocals", "drums", "bass", "guitar", "piano", "other"), "archive": "athena-6-stems-experimental.zip", "engine": "demucs"}, } +TARGETS = { + "vocals": {"mode": "vocals", "stem": "vocals", "remainder": "instrumental", "archive": "athena-gesang-und-rest.zip", "rest_file": "instrumental.flac"}, + "drums": {"mode": "four_stem", "stem": "drums", "archive": "athena-schlagzeug-und-rest.zip", "rest_file": "rest-ohne-schlagzeug.flac"}, + "bass": {"mode": "four_stem", "stem": "bass", "archive": "athena-bass-und-rest.zip", "rest_file": "rest-ohne-bass.flac"}, + "guitar": {"mode": "six_stem", "stem": "guitar", "archive": "athena-gitarre-und-rest.zip", "rest_file": "rest-ohne-gitarre.flac"}, + "piano": {"mode": "six_stem", "stem": "piano", "archive": "athena-piano-und-rest.zip", "rest_file": "rest-ohne-piano.flac"}, +} app = FastAPI(title="Athena Stem Separator", version="2.0") @@ -43,6 +50,7 @@ def health() -> dict: "status": "ok" if all(available.values()) else "starting", "models": {name: mode["model"] for name, mode in MODES.items()}, "models_ready": available, + "targets": list(TARGETS), "busy": SEPARATION_LOCK.locked(), "uptime_seconds": round(time.time() - STARTED, 1), } @@ -80,11 +88,34 @@ def _stem_name(path: Path, expected: tuple[str, ...]) -> str | None: return None +def _mix_remainder(stems: list[Path], output_path: Path) -> None: + args = ["ffmpeg", "-hide_banner", "-loglevel", "error", "-y"] + for stem in stems: + args.extend(["-i", str(stem)]) + inputs = "".join(f"[{index}:a]" for index in range(len(stems))) + args.extend([ + "-filter_complex", f"{inputs}amix=inputs={len(stems)}:normalize=0:dropout_transition=0[rest]", + "-map", "[rest]", "-ar", "44100", "-c:a", "flac", str(output_path), + ]) + completed = subprocess.run(args, capture_output=True, text=True, timeout=1800) + if completed.returncode: + detail = (completed.stderr or completed.stdout or "unknown ffmpeg error")[-4000:] + raise RuntimeError(f"Restspur konnte nicht erzeugt werden: {detail}") + + @app.post("/v1/separate") -async def separate(file: UploadFile = File(...), mode: str = Form("vocals")) -> FileResponse: - selected_mode = MODES.get(mode) +async def separate( + file: UploadFile = File(...), + target: str | None = Form(None), + mode: str | None = Form(None), +) -> FileResponse: + selected_target = TARGETS.get(target) if target else None + if target and selected_target is None: + raise HTTPException(422, f"Unbekannte Zielspur: {target}") + selected_mode_name = selected_target["mode"] if selected_target else (mode or "vocals") + selected_mode = MODES.get(selected_mode_name) if selected_mode is None: - raise HTTPException(422, f"Unbekannter Trennmodus: {mode}") + raise HTTPException(422, f"Unbekannter Trennmodus: {selected_mode_name}") suffix = Path(file.filename or "upload.wav").suffix.lower() if suffix not in ALLOWED: raise HTTPException(415, "Dieses Audioformat wird nicht unterstützt.") @@ -114,14 +145,30 @@ async def separate(file: UploadFile = File(...), mode: str = Form("vocals")) -> if missing: found = ", ".join(stem.name for stem in stems) or "keine" raise RuntimeError(f"Fehlende Spuren: {', '.join(missing)}; gefunden: {found}") - archive = job / selected_mode["archive"] + archive_name = selected_target["archive"] if selected_target else selected_mode["archive"] + archive = job / archive_name with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_STORED) as bundle: - for stem in expected: - bundle.write(recognized[stem], f"{stem}.flac") + if selected_target: + target_stem = selected_target["stem"] + bundle.write(recognized[target_stem], f"{target_stem}.flac") + if "remainder" in selected_target: + remainder = recognized[selected_target["remainder"]] + else: + remainder = job / selected_target["rest_file"] + await asyncio.to_thread( + _mix_remainder, + [recognized[stem] for stem in expected if stem != target_stem], + remainder, + ) + bundle.write(remainder, selected_target["rest_file"]) + else: + # Rückwärtskompatibilität für bestehende API-Clients. + for stem in expected: + bundle.write(recognized[stem], f"{stem}.flac") return FileResponse( archive, media_type="application/zip", - filename=selected_mode["archive"], + filename=archive_name, background=BackgroundTask(_cleanup, job), ) except HTTPException: diff --git a/experiments/bs-roformer-vocal-separation/index.html b/experiments/bs-roformer-vocal-separation/index.html index f972bad..b0ec9a5 100644 --- a/experiments/bs-roformer-vocal-separation/index.html +++ b/experiments/bs-roformer-vocal-separation/index.html @@ -1,9 +1,15 @@
-Wähle zwischen der besonders sauberen Gesangstrennung und zusammengehörigen Mehrspur-Modellen.
- -