feat: add multi-stem audio separation modes

This commit is contained in:
Mikei386
2026-09-09 00:30:08 +02:00
parent 30203bf13b
commit e2f35517f8
6 changed files with 76 additions and 40 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ Details, Installation, Prüfung und Rollback stehen in
- Athena-Dashboard: `http://192.168.1.212:8099`
- Musikstudio, Original UI (stabil): `http://192.168.1.212:7862`
- Musikstudio, Community UI (experimentell): `http://192.168.1.212:7861`
- Stimmen trennen (BS-RoFormer): `http://192.168.1.212:8007`
- Spuren trennen (BS-RoFormer + Demucs, 2/4/6 Stems): `http://192.168.1.212:8007`
Der Betriebsmodus lässt sich dort direkt umschalten. In Hermes funktionieren
außerdem `/athena music`, `/athena stems`, `/athena llm` und `/athena status`; Details stehen in
+8 -5
View File
@@ -4,8 +4,8 @@ Athena besitzt drei gegenseitig exklusive Betriebsmodi:
- `llm`: ein llama.cpp-Profil und Qwen3-TTS laufen; Spezialdienste sind gestoppt.
- `music`: ACE-Step 1.5 XL-SFT läuft; alle LLM-, Bild-, TTS- und Separator-Worker sind gestoppt.
- `separation`: BS-RoFormer Viperx 1297 trennt Gesang und Instrumental;
LLM, Bild, TTS und ACE-Step sind gestoppt.
- `separation`: wahlweise BS-RoFormer für Gesang/Instrumental oder Demucs für
vier beziehungsweise sechs Spuren; LLM, Bild, TTS und ACE-Step sind gestoppt.
Die Zustandsmaschine lebt im Athena-Router. Das Dashboard und Chat-Clients wie
Hermes sind nur Bedienoberflächen derselben API. Der zuletzt aktive LLM-Modus
@@ -36,9 +36,12 @@ 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 ein ZIP mit verlustfreien `vocals.flac` und
`instrumental.flac`. Grundlage ist `audio-separator` 0.47.0 mit
`model_bs_roformer_ep_317_sdr_12.9755.ckpt`.
ü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`.
Hermes benötigt dafür kein Plugin. Exakt eingegebene Steuerbefehle werden vom
Router lokal beantwortet, auch wenn gerade kein LLM geladen ist:
@@ -19,4 +19,4 @@ ENV MODEL_FILENAME=model_bs_roformer_ep_317_sdr_12.9755.ckpt \
JOB_DIR=/data/jobs
EXPOSE 8080
CMD ["sh", "-c", "audio-separator --model_filename \"$MODEL_FILENAME\" --model_file_dir \"$MODEL_DIR\" --download_model_only && exec uvicorn app:app --host 0.0.0.0 --port 8080 --workers 1"]
CMD ["sh", "-c", "for model in \"$MODEL_FILENAME\" htdemucs_ft.yaml htdemucs_6s.yaml; do audio-separator --model_filename \"$model\" --model_file_dir \"$MODEL_DIR\" --download_model_only || exit 1; done; exec uvicorn app:app --host 0.0.0.0 --port 8080 --workers 1"]
@@ -1,16 +1,23 @@
# Athena Vocal Separator
# Athena Stem Separator
Exklusiver dritter Athena-Betriebsmodus für lokale Zwei-Spur-Trennung in
`vocals.flac` und `instrumental.flac`.
Exklusiver dritter Athena-Betriebsmodus für lokale Zwei- bis Sechs-Spur-Trennung.
- Engine: `audio-separator` 0.47.0 (MIT)
- Modell: BS-RoFormer Viperx 1297,
`model_bs_roformer_ep_317_sdr_12.9755.ckpt`
- Modellbewertung im audio-separator-Katalog: Vocal SDR 12,9,
Instrumental SDR 17,0
- **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.
- GPU: RTX 5080; LLM, Bildmodelle, TTS und ACE-Step sind dabei verriegelt.
- Privat erreichbar: `http://192.168.1.212:8007/`
Das Modell wird beim ersten Start nach `/data/models/audio-separator`
Die Modelle werden beim ersten Start nach `/data/models/audio-separator`
heruntergeladen. Temporäre Jobs liegen unter `/data/audio/separation` und
werden nach dem ZIP-Download entfernt.
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`.
+42 -21
View File
@@ -9,7 +9,7 @@ import time
import zipfile
from pathlib import Path
from fastapi import FastAPI, File, HTTPException, UploadFile
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, HTMLResponse
from starlette.background import BackgroundTask
@@ -22,7 +22,13 @@ ALLOWED = {".wav", ".flac", ".mp3", ".m4a", ".aac", ".ogg", ".opus", ".wma"}
SEPARATION_LOCK = asyncio.Lock()
STARTED = time.time()
app = FastAPI(title="Athena Vocal Separator", version="1.0")
MODES = {
"vocals": {"model": MODEL, "stems": ("vocals", "instrumental"), "archive": "athena-vocals-instrumental.zip", "engine": "mdxc"},
"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"},
}
app = FastAPI(title="Athena Stem Separator", version="2.0")
@app.get("/", response_class=HTMLResponse)
@@ -32,11 +38,11 @@ def index() -> str:
@app.get("/health")
def health() -> dict:
checkpoint = MODEL_DIR / MODEL
available = {name: (MODEL_DIR / mode["model"]).exists() for name, mode in MODES.items()}
return {
"status": "ok" if checkpoint.exists() else "starting",
"model": MODEL,
"model_ready": checkpoint.exists(),
"status": "ok" if all(available.values()) else "starting",
"models": {name: mode["model"] for name, mode in MODES.items()},
"models_ready": available,
"busy": SEPARATION_LOCK.locked(),
"uptime_seconds": round(time.time() - STARTED, 1),
}
@@ -46,28 +52,40 @@ def _cleanup(path: Path) -> None:
shutil.rmtree(path, ignore_errors=True)
def _run_separator(input_path: Path, output_dir: Path) -> None:
def _run_separator(input_path: Path, output_dir: Path, mode: dict) -> None:
args = [
"audio-separator", str(input_path),
"--model_filename", MODEL,
"--model_filename", mode["model"],
"--model_file_dir", str(MODEL_DIR),
"--output_dir", str(output_dir),
"--output_format", "FLAC",
"--sample_rate", "44100",
"--use_soundfile",
"--use_autocast",
"--mdxc_segment_size", "256",
"--mdxc_overlap", "8",
"--mdxc_batch_size", "1",
]
if mode["engine"] == "mdxc":
args.extend(["--mdxc_segment_size", "256", "--mdxc_overlap", "8", "--mdxc_batch_size", "1"])
else:
args.extend(["--demucs_segment_size", "40", "--demucs_shifts", "2", "--demucs_overlap", "0.25"])
completed = subprocess.run(args, capture_output=True, text=True, timeout=7200)
if completed.returncode:
detail = (completed.stderr or completed.stdout or "unknown error")[-4000:]
raise RuntimeError(detail)
def _stem_name(path: Path, expected: tuple[str, ...]) -> str | None:
lower = path.stem.lower()
for stem in sorted(expected, key=len, reverse=True):
if stem in lower:
return stem
return None
@app.post("/v1/separate")
async def separate(file: UploadFile = File(...)) -> FileResponse:
async def separate(file: UploadFile = File(...), mode: str = Form("vocals")) -> FileResponse:
selected_mode = MODES.get(mode)
if selected_mode is None:
raise HTTPException(422, f"Unbekannter Trennmodus: {mode}")
suffix = Path(file.filename or "upload.wav").suffix.lower()
if suffix not in ALLOWED:
raise HTTPException(415, "Dieses Audioformat wird nicht unterstützt.")
@@ -87,21 +105,24 @@ async def separate(file: UploadFile = File(...)) -> FileResponse:
raise HTTPException(413, "Datei ist größer als 1 GiB.")
handle.write(chunk)
async with SEPARATION_LOCK:
await asyncio.to_thread(_run_separator, input_path, output_dir)
await asyncio.to_thread(_run_separator, input_path, output_dir, selected_mode)
stems = sorted(output_dir.glob("*.flac"))
if len(stems) != 2:
raise RuntimeError(f"Erwartet wurden zwei FLAC-Dateien, gefunden: {len(stems)}")
archive = job / "athena-vocals-instrumental.zip"
expected = selected_mode["stems"]
recognized = {_stem_name(stem, expected): stem for stem in stems}
recognized.pop(None, None)
missing = [stem for stem in expected if stem not in recognized]
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"]
with zipfile.ZipFile(archive, "w", compression=zipfile.ZIP_STORED) as bundle:
for stem in stems:
lower = stem.name.lower()
target = "vocals.flac" if "vocal" in lower else "instrumental.flac"
bundle.write(stem, target)
for stem in expected:
bundle.write(recognized[stem], f"{stem}.flac")
return FileResponse(
archive,
media_type="application/zip",
filename="athena-vocals-instrumental.zip",
filename=selected_mode["archive"],
background=BackgroundTask(_cleanup, job),
)
except HTTPException:
@@ -1,4 +1,9 @@
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena · Stimmen trennen</title><style>
:root{color-scheme:dark;--bg:#07111c;--card:#101d2b;--line:#26384b;--cyan:#48d7f5;--mint:#63e6be;--text:#ecf5ff;--muted:#91a4b7}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#142a42 0,#07111c 42%);font:16px system-ui,sans-serif;color:var(--text);min-height:100vh;display:grid;place-items:center;padding:24px}.card{width:min(760px,100%);padding:32px;border:1px solid var(--line);border-radius:22px;background:rgba(16,29,43,.96);box-shadow:0 25px 70px #0008}.eyebrow{color:var(--cyan);font-weight:800;letter-spacing:.14em;text-transform:uppercase;font-size:12px}h1{font-size:clamp(30px,5vw,52px);margin:.3em 0 .15em}p{color:var(--muted);line-height:1.6}.drop{display:block;margin:28px 0;padding:44px 24px;border:2px dashed #3f5a72;border-radius:18px;text-align:center;cursor:pointer;transition:.2s}.drop:hover,.drop.drag{border-color:var(--cyan);background:#48d7f50b}input{display:none}.file{color:var(--mint);font-weight:700;margin-top:8px}button{width:100%;border:0;border-radius:13px;padding:15px;font-weight:800;font-size:16px;background:linear-gradient(90deg,var(--cyan),var(--mint));color:#05202a;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}.status{min-height:28px;margin-top:18px;color:var(--muted)}.bar{height:7px;background:#07111c;border-radius:9px;overflow:hidden;margin-top:12px}.fill{height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--mint));transition:.4s}.run .fill{width:85%;animation:pulse 1.5s infinite alternate}@keyframes pulse{to{opacity:.45}}small{display:block;color:#71879a;margin-top:20px}</style></head><body><main class="card"><div class="eyebrow">Athena Audio Lab</div><h1>Stimmen sauber trennen</h1><p>BS‑RoFormer Viperx 1297 zerlegt deinen Titel in zwei verlustfreie FLAC-Spuren: Gesang und Instrumental.</p><label class="drop" id="drop">Audio auswählen oder hier ablegen<input id="file" type="file" accept="audio/*"><div class="file" id="name">Noch keine Datei gewählt</div></label><button id="start" disabled>Vocals und Instrumental erzeugen</button><div class="status" id="status">Bereit.</div><div class="bar" id="bar"><div class="fill"></div></div><small>Die Verarbeitung läuft lokal auf Athena. Nichts wird in eine Cloud hochgeladen.</small></main><script>
const file=document.querySelector('#file'),drop=document.querySelector('#drop'),name=document.querySelector('#name'),start=document.querySelector('#start'),status=document.querySelector('#status'),bar=document.querySelector('#bar');let selected;function choose(f){selected=f;name.textContent=f?`${f.name} · ${(f.size/1048576).toFixed(1)} MiB`:'Noch keine Datei gewählt';start.disabled=!f}file.onchange=()=>choose(file.files[0]);drop.ondragover=e=>{e.preventDefault();drop.classList.add('drag')};drop.ondragleave=()=>drop.classList.remove('drag');drop.ondrop=e=>{e.preventDefault();drop.classList.remove('drag');choose(e.dataTransfer.files[0])};start.onclick=async()=>{start.disabled=true;bar.classList.add('run');status.textContent='Modell trennt den Titel – das kann einige Minuten dauern …';let body=new FormData();body.append('file',selected);try{let r=await fetch('/v1/separate',{method:'POST',body});if(!r.ok)throw Error((await r.json()).detail||`HTTP ${r.status}`);let blob=await r.blob(),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='athena-vocals-instrumental.zip';a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000);status.textContent='Fertig – ZIP mit vocals.flac und instrumental.flac wurde geladen.'}catch(e){status.textContent=`Fehler: ${e.message}`}finally{bar.classList.remove('run');start.disabled=false}};
<!doctype html>
<html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Athena · Spuren trennen</title><style>
:root{color-scheme:dark;--bg:#07111c;--card:#101d2b;--line:#26384b;--cyan:#48d7f5;--mint:#63e6be;--text:#ecf5ff;--muted:#91a4b7}*{box-sizing:border-box}body{margin:0;background:radial-gradient(circle at 20% 0,#142a42 0,#07111c 42%);font:16px system-ui,sans-serif;color:var(--text);min-height:100vh;display:grid;place-items:center;padding:24px}.card{width:min(820px,100%);padding:32px;border:1px solid var(--line);border-radius:22px;background:rgba(16,29,43,.96);box-shadow:0 25px 70px #0008}.eyebrow{color:var(--cyan);font-weight:800;letter-spacing:.14em;text-transform:uppercase;font-size:12px}h1{font-size:clamp(30px,5vw,52px);margin:.3em 0 .15em}p{color:var(--muted);line-height:1.6}.modes{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin:24px 0}.mode{display:block;border:1px solid var(--line);border-radius:14px;padding:14px;cursor:pointer}.mode:has(input:checked){border-color:var(--cyan);background:#48d7f510}.mode input{display:none}.mode b,.mode span{display:block}.mode span{color:var(--muted);font-size:13px;margin-top:5px;line-height:1.35}.drop{display:block;margin:20px 0;padding:36px 24px;border:2px dashed #3f5a72;border-radius:18px;text-align:center;cursor:pointer;transition:.2s}.drop:hover,.drop.drag{border-color:var(--cyan);background:#48d7f50b}.drop input{display:none}.file{color:var(--mint);font-weight:700;margin-top:8px}button{width:100%;border:0;border-radius:13px;padding:15px;font-weight:800;font-size:16px;background:linear-gradient(90deg,var(--cyan),var(--mint));color:#05202a;cursor:pointer}button:disabled{opacity:.45;cursor:not-allowed}.status{min-height:28px;margin-top:18px;color:var(--muted)}.bar{height:7px;background:#07111c;border-radius:9px;overflow:hidden;margin-top:12px}.fill{height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--mint));transition:.4s}.run .fill{width:85%;animation:pulse 1.5s infinite alternate}@keyframes pulse{to{opacity:.45}}small{display:block;color:#71879a;margin-top:20px}@media(max-width:680px){.modes{grid-template-columns:1fr}}
</style></head><body><main class="card"><div class="eyebrow">Athena Audio Lab</div><h1>Instrumente und Stimmen trennen</h1><p>Wähle zwischen der besonders sauberen Gesangstrennung und zusammengehörigen Mehrspur-Modellen.</p>
<div class="modes"><label class="mode"><input type="radio" name="mode" value="vocals" checked><b>Gesang · beste Qualität</b><span>Vocals + Instrumental<br>BS‑RoFormer</span></label><label class="mode"><input type="radio" name="mode" value="four_stem"><b>4 Spuren</b><span>Gesang, Schlagzeug, Bass, Rest<br>HTDemucs FT</span></label><label class="mode"><input type="radio" name="mode" value="six_stem"><b>6 Spuren · experimentell</b><span>Zusätzlich Gitarre und Piano<br>HTDemucs 6s</span></label></div>
<label class="drop" id="drop">Audio auswählen oder hier ablegen<input id="file" type="file" accept="audio/*"><div class="file" id="name">Noch keine Datei gewählt</div></label><button id="start" disabled>Spuren erzeugen</button><div class="status" id="status">Bereit.</div><div class="bar" id="bar"><div class="fill"></div></div><small>Alles läuft lokal auf Athena. Synthesizer, Streicher sowie elektrische und akustische Gitarre separat benötigen zusätzliche Spezialmodelle und sind noch nicht freigeschaltet.</small></main><script>
const file=document.querySelector('#file'),drop=document.querySelector('#drop'),name=document.querySelector('#name'),start=document.querySelector('#start'),status=document.querySelector('#status'),bar=document.querySelector('#bar');let selected;const info={vocals:['athena-vocals-instrumental.zip','vocals.flac und instrumental.flac'],four_stem:['athena-4-stems.zip','vier Spuren'],six_stem:['athena-6-stems-experimental.zip','sechs Spuren']};function choose(f){selected=f;name.textContent=f?`${f.name} · ${(f.size/1048576).toFixed(1)} MiB`:'Noch keine Datei gewählt';start.disabled=!f}file.onchange=()=>choose(file.files[0]);drop.ondragover=e=>{e.preventDefault();drop.classList.add('drag')};drop.ondragleave=()=>drop.classList.remove('drag');drop.ondrop=e=>{e.preventDefault();drop.classList.remove('drag');choose(e.dataTransfer.files[0])};start.onclick=async()=>{const mode=document.querySelector('input[name=mode]:checked').value;start.disabled=true;bar.classList.add('run');status.textContent='Modell trennt den Titel – das kann einige Minuten dauern …';let body=new FormData();body.append('file',selected);body.append('mode',mode);try{let r=await fetch('/v1/separate',{method:'POST',body});if(!r.ok)throw Error((await r.json()).detail||`HTTP ${r.status}`);let blob=await r.blob(),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download=info[mode][0];a.click();setTimeout(()=>URL.revokeObjectURL(a.href),5000);status.textContent=`Fertig – ZIP mit ${info[mode][1]} wurde geladen.`}catch(e){status.textContent=`Fehler: ${e.message}`}finally{bar.classList.remove('run');start.disabled=false}};
</script></body></html>