Add BS-RoFormer vocal separation mode

This commit is contained in:
Mikei386
2026-09-08 19:23:29 +02:00
parent a4e894fe70
commit 0069b61dbb
15 changed files with 421 additions and 52 deletions
+89 -35
View File
@@ -109,6 +109,7 @@ PROFILE_CONTROL_TOKEN_FILE = os.environ.get(
ENABLE_MUSIC_MODE = os.environ.get(
"ENABLE_MUSIC_MODE", "false").lower() in {"1", "true", "yes"}
MUSIC_START_TIMEOUT = float(os.environ.get("MUSIC_START_TIMEOUT", "600"))
SEPARATOR_START_TIMEOUT = float(os.environ.get("SEPARATOR_START_TIMEOUT", "600"))
# Optional worker APIs. The clean Docker baseline deliberately ships only
# text/multimodal chat; absent workers must fail explicitly instead of trying
@@ -339,6 +340,27 @@ def _music_worker_health() -> str:
return "unknown"
def _separator_worker_state() -> str:
if not PROFILE_CONTROL_URL:
return "unsupported"
try:
return str(_profile_controller_request("GET", "/status").get(
"separator_worker", "missing"))
except Exception as exc:
log.warning("Stem-Separator-Status nicht verfügbar: %s", exc)
return "unknown"
def _separator_worker_health() -> str:
if not PROFILE_CONTROL_URL:
return "unsupported"
try:
return str(_profile_controller_request("GET", "/status").get(
"separator_health", "unknown"))
except Exception:
return "unknown"
def _wait_music_ready() -> None:
deadline = time.monotonic() + MUSIC_START_TIMEOUT
while time.monotonic() < deadline:
@@ -353,38 +375,57 @@ def _wait_music_ready() -> None:
f"ACE-Step nach {MUSIC_START_TIMEOUT:.0f} s nicht bereit")
def _wait_separator_ready() -> None:
deadline = time.monotonic() + SEPARATOR_START_TIMEOUT
while time.monotonic() < deadline:
status = _profile_controller_request("GET", "/status")
if (status.get("separator_worker") == "running"
and status.get("separator_health") == "healthy"):
return
if status.get("separator_health") == "unhealthy":
raise RuntimeError("BS-RoFormer-Container ist unhealthy")
time.sleep(POLL_INTERVAL)
raise RuntimeError(
f"BS-RoFormer nach {SEPARATOR_START_TIMEOUT:.0f} s nicht bereit")
def set_operating_mode(mode: str) -> dict:
"""Atomarer Wechsel zwischen llama.cpp/TTS und ACE-Step Studio."""
"""Atomarer Wechsel zwischen LLM, ACE-Step und Stem-Separation."""
if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL:
raise RuntimeError("Musikmodus ist nicht konfiguriert")
if mode not in {"llm", "music"}:
raise ValueError("Modus muss 'llm' oder 'music' sein")
if mode not in {"llm", "music", "separation"}:
raise ValueError("Modus muss 'llm', 'music' oder 'separation' sein")
with STATE.lock:
STATE.mode_error = None
if mode == "music":
if STATE.mode == "music" and _music_worker_state() == "running":
return {"status": "ok", "mode": "music", "changed": False}
if mode in {"music", "separation"}:
worker_state = (_music_worker_state() if mode == "music"
else _separator_worker_state())
if STATE.mode == mode and worker_state == "running":
return {"status": "ok", "mode": mode, "changed": False}
profile = current_profile()
saved = RUNTIME.load().get("last_profile")
previous = RUNTIME.load()
saved = previous.get("return_profile") or previous.get("last_profile")
return_profile = profile if profile in PROFILES else saved
if return_profile not in PROFILES:
return_profile = next(iter(PROFILES))
STATE.mode_phase = "starting-music"
STATE.mode_phase = f"starting-{mode}"
_set_qwen_unavailable(True)
try:
# Persist intent before stopping anything so a router restart
# during ACE-Step loading can resume the same transition.
RUNTIME.save(mode="music", return_profile=return_profile,
RUNTIME.save(mode=mode, return_profile=return_profile,
last_profile=return_profile,
phase="starting-music")
phase=f"starting-{mode}")
_wait_chats_drained()
_profile_controller_request("POST", "/workers/music/start")
_wait_music_ready()
STATE.mode = "music"
path = ("/workers/music/start" if mode == "music"
else "/workers/separator/start")
_profile_controller_request("POST", path)
_wait_music_ready() if mode == "music" else _wait_separator_ready()
STATE.mode = mode
STATE.mode_phase = "ready"
RUNTIME.save(mode="music", return_profile=return_profile,
last_profile=return_profile, phase="music")
return {"status": "ok", "mode": "music", "changed": True,
RUNTIME.save(mode=mode, return_profile=return_profile,
last_profile=return_profile, phase=mode)
return {"status": "ok", "mode": mode, "changed": True,
"return_profile": return_profile}
except Exception as exc:
STATE.mode_error = str(exc)
@@ -399,6 +440,7 @@ def set_operating_mode(mode: str) -> dict:
_set_qwen_unavailable(True)
try:
_profile_controller_request("POST", "/workers/music/stop")
_profile_controller_request("POST", "/workers/separator/stop")
_restore_qwen(profile)
STATE.mode = "llm"
STATE.mode_phase = "ready"
@@ -417,14 +459,14 @@ def schedule_operating_mode(mode: str) -> tuple[bool, str]:
"""Start a transition in the background so chat/UI acknowledgement is instant."""
if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL:
raise RuntimeError("Musikmodus ist nicht konfiguriert")
if mode not in {"llm", "music"}:
raise ValueError("Modus muss 'llm' oder 'music' sein")
if mode not in {"llm", "music", "separation"}:
raise ValueError("Modus muss 'llm', 'music' oder 'separation' sein")
with STATE.lock:
if STATE.mode_phase not in {"ready", "error"}:
return False, STATE.mode_phase
if STATE.mode == mode and STATE.mode_phase == "ready":
return False, "ready"
STATE.mode_phase = "starting-music" if mode == "music" else "restoring-llm"
STATE.mode_phase = f"starting-{mode}" if mode != "llm" else "restoring-llm"
def transition() -> None:
try:
@@ -452,7 +494,9 @@ def _control_command(data: dict, path: str) -> str | None:
if not isinstance(text, str):
return None
command = text.strip().casefold()
return command if command in {"/athena music", "/athena llm", "/athena status"} else None
return command if command in {"/athena music", "/athena stems",
"/athena separation", "/athena llm",
"/athena status"} else None
# ---------------------------------------------------------------------------
@@ -1925,6 +1969,8 @@ class Handler(BaseHTTPRequestHandler):
"phase": STATE.mode_phase,
"music_worker": _music_worker_state(),
"music_health": _music_worker_health(),
"separator_worker": _separator_worker_state(),
"separator_health": _separator_worker_health(),
"return_profile": state.get("return_profile"),
"last_error": STATE.mode_error,
"enabled": ENABLE_MUSIC_MODE,
@@ -1934,8 +1980,8 @@ class Handler(BaseHTTPRequestHandler):
try:
data = json.loads(self._read_body() or b"{}")
mode = data.get("mode") if isinstance(data, dict) else None
if mode not in {"llm", "music"}:
raise ValueError("Feld 'mode' muss 'llm' oder 'music' sein")
if mode not in {"llm", "music", "separation"}:
raise ValueError("Feld 'mode' muss 'llm', 'music' oder 'separation' sein")
started, phase = schedule_operating_mode(mode)
self._send_json(202 if started else 200, {
"status": "accepted" if started else "ok",
@@ -2597,16 +2643,20 @@ class Handler(BaseHTTPRequestHandler):
profile = current_profile()
text = (f"Athena läuft im {mode['active'].upper()}-Modus. "
f"Phase: {mode['phase']}. Musik-Worker: "
f"{mode['music_worker']}. LLM-Profil: {profile or 'entladen'}.")
f"{mode['music_worker']}. Stem-Separator: "
f"{mode['separator_worker']}. LLM-Profil: {profile or 'entladen'}.")
else:
target = "music" if command == "/athena music" else "llm"
target = ("music" if command == "/athena music" else
"separation" if command in {"/athena stems", "/athena separation"}
else "llm")
try:
started, phase = schedule_operating_mode(target)
if started:
text = ("Musikstudio wird gestartet. Das LLM und TTS werden "
"entladen; der Fortschritt ist im Athena-Dashboard sichtbar."
text = ("Musikstudio wird gestartet. LLM und TTS werden entladen."
if target == "music" else
"Musikstudio wird beendet und das vorherige LLM-Profil wird wiederhergestellt.")
"Stimmtrennung wird gestartet. LLM und TTS werden entladen."
if target == "separation" else
"Spezialmodus wird beendet und das vorherige LLM-Profil wiederhergestellt.")
else:
text = (f"Athena ist bereits im {target.upper()}-Modus "
f"oder wechselt gerade ({phase}).")
@@ -2897,20 +2947,24 @@ def _startup_reconcile() -> None:
if removed:
log.info("Startup-Retention: %d alte Bilder entfernt", len(removed))
if ENABLE_MUSIC_MODE and previous.get("mode") == "music":
STATE.mode = "music"
STATE.mode_phase = "starting-music"
special_mode = previous.get("mode")
if ENABLE_MUSIC_MODE and special_mode in {"music", "separation"}:
STATE.mode = special_mode
STATE.mode_phase = f"starting-{special_mode}"
_set_qwen_unavailable(True)
try:
_profile_controller_request("POST", "/workers/music/start")
_wait_music_ready()
path = ("/workers/music/start" if special_mode == "music"
else "/workers/separator/start")
_profile_controller_request("POST", path)
_wait_music_ready() if special_mode == "music" else _wait_separator_ready()
STATE.mode_phase = "ready"
RUNTIME.save(mode="music", phase="music")
log.info("Recovery: Musikmodus wiederhergestellt")
RUNTIME.save(mode=special_mode, phase=special_mode)
log.info("Recovery: Spezialmodus %s wiederhergestellt", special_mode)
except Exception as exc:
STATE.mode_error = str(exc)
STATE.mode_phase = "error"
log.error("Recovery: Musikmodus konnte nicht gestartet werden: %s", exc)
log.error("Recovery: Spezialmodus %s konnte nicht gestartet werden: %s",
special_mode, exc)
return
profile = current_profile()