Add private Vevo2 voice studio mode
This commit is contained in:
+68
-18
@@ -110,6 +110,7 @@ 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"))
|
||||
VOICE_START_TIMEOUT = float(os.environ.get("VOICE_START_TIMEOUT", "600"))
|
||||
|
||||
# Optional worker APIs. The clean Docker baseline deliberately ships only
|
||||
# text/multimodal chat; absent workers must fail explicitly instead of trying
|
||||
@@ -361,6 +362,27 @@ def _separator_worker_health() -> str:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _voice_worker_state() -> str:
|
||||
if not PROFILE_CONTROL_URL:
|
||||
return "unsupported"
|
||||
try:
|
||||
return str(_profile_controller_request("GET", "/status").get(
|
||||
"voice_worker", "missing"))
|
||||
except Exception as exc:
|
||||
log.warning("Voice-Worker-Status nicht verfügbar: %s", exc)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _voice_worker_health() -> str:
|
||||
if not PROFILE_CONTROL_URL:
|
||||
return "unsupported"
|
||||
try:
|
||||
return str(_profile_controller_request("GET", "/status").get(
|
||||
"voice_health", "unknown"))
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _wait_music_ready() -> None:
|
||||
deadline = time.monotonic() + MUSIC_START_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
@@ -389,17 +411,40 @@ def _wait_separator_ready() -> None:
|
||||
f"BS-RoFormer nach {SEPARATOR_START_TIMEOUT:.0f} s nicht bereit")
|
||||
|
||||
|
||||
def _wait_voice_ready() -> None:
|
||||
deadline = time.monotonic() + VOICE_START_TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
status = _profile_controller_request("GET", "/status")
|
||||
if (status.get("voice_worker") == "running"
|
||||
and status.get("voice_health") == "healthy"):
|
||||
return
|
||||
if status.get("voice_health") == "unhealthy":
|
||||
raise RuntimeError("Vevo2-Container ist unhealthy")
|
||||
time.sleep(POLL_INTERVAL)
|
||||
raise RuntimeError(
|
||||
f"Vevo2 nach {VOICE_START_TIMEOUT:.0f} s nicht bereit")
|
||||
|
||||
|
||||
def _special_worker(mode: str) -> tuple[str, str, callable]:
|
||||
if mode == "music":
|
||||
return "/workers/music/start", _music_worker_state(), _wait_music_ready
|
||||
if mode == "separation":
|
||||
return "/workers/separator/start", _separator_worker_state(), _wait_separator_ready
|
||||
if mode == "voice":
|
||||
return "/workers/voice/start", _voice_worker_state(), _wait_voice_ready
|
||||
raise ValueError(f"unbekannter Spezialmodus: {mode}")
|
||||
|
||||
|
||||
def set_operating_mode(mode: str) -> dict:
|
||||
"""Atomarer Wechsel zwischen LLM, ACE-Step und Stem-Separation."""
|
||||
"""Atomarer Wechsel zwischen LLM und den exklusiven GPU-Werkzeugen."""
|
||||
if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL:
|
||||
raise RuntimeError("Musikmodus ist nicht konfiguriert")
|
||||
if mode not in {"llm", "music", "separation"}:
|
||||
raise ValueError("Modus muss 'llm', 'music' oder 'separation' sein")
|
||||
if mode not in {"llm", "music", "separation", "voice"}:
|
||||
raise ValueError("Modus muss 'llm', 'music', 'separation' oder 'voice' sein")
|
||||
with STATE.lock:
|
||||
STATE.mode_error = None
|
||||
if mode in {"music", "separation"}:
|
||||
worker_state = (_music_worker_state() if mode == "music"
|
||||
else _separator_worker_state())
|
||||
if mode in {"music", "separation", "voice"}:
|
||||
path, worker_state, wait_ready = _special_worker(mode)
|
||||
if STATE.mode == mode and worker_state == "running":
|
||||
return {"status": "ok", "mode": mode, "changed": False}
|
||||
profile = current_profile()
|
||||
@@ -417,10 +462,8 @@ def set_operating_mode(mode: str) -> dict:
|
||||
last_profile=return_profile,
|
||||
phase=f"starting-{mode}")
|
||||
_wait_chats_drained()
|
||||
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()
|
||||
wait_ready()
|
||||
STATE.mode = mode
|
||||
STATE.mode_phase = "ready"
|
||||
RUNTIME.save(mode=mode, return_profile=return_profile,
|
||||
@@ -441,6 +484,7 @@ def set_operating_mode(mode: str) -> dict:
|
||||
try:
|
||||
_profile_controller_request("POST", "/workers/music/stop")
|
||||
_profile_controller_request("POST", "/workers/separator/stop")
|
||||
_profile_controller_request("POST", "/workers/voice/stop")
|
||||
_restore_qwen(profile)
|
||||
STATE.mode = "llm"
|
||||
STATE.mode_phase = "ready"
|
||||
@@ -459,8 +503,8 @@ 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", "separation"}:
|
||||
raise ValueError("Modus muss 'llm', 'music' oder 'separation' sein")
|
||||
if mode not in {"llm", "music", "separation", "voice"}:
|
||||
raise ValueError("Modus muss 'llm', 'music', 'separation' oder 'voice' sein")
|
||||
with STATE.lock:
|
||||
if STATE.mode_phase not in {"ready", "error"}:
|
||||
return False, STATE.mode_phase
|
||||
@@ -496,6 +540,7 @@ def _control_command(data: dict, path: str) -> str | None:
|
||||
command = text.strip().casefold()
|
||||
return command if command in {"/athena music", "/athena stems",
|
||||
"/athena separation", "/athena llm",
|
||||
"/athena voice",
|
||||
"/athena status"} else None
|
||||
|
||||
|
||||
@@ -1971,6 +2016,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"music_health": _music_worker_health(),
|
||||
"separator_worker": _separator_worker_state(),
|
||||
"separator_health": _separator_worker_health(),
|
||||
"voice_worker": _voice_worker_state(),
|
||||
"voice_health": _voice_worker_health(),
|
||||
"return_profile": state.get("return_profile"),
|
||||
"last_error": STATE.mode_error,
|
||||
"enabled": ENABLE_MUSIC_MODE,
|
||||
@@ -1980,8 +2027,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", "separation"}:
|
||||
raise ValueError("Feld 'mode' muss 'llm', 'music' oder 'separation' sein")
|
||||
if mode not in {"llm", "music", "separation", "voice"}:
|
||||
raise ValueError("Feld 'mode' muss 'llm', 'music', 'separation' oder 'voice' sein")
|
||||
started, phase = schedule_operating_mode(mode)
|
||||
self._send_json(202 if started else 200, {
|
||||
"status": "accepted" if started else "ok",
|
||||
@@ -2644,10 +2691,12 @@ class Handler(BaseHTTPRequestHandler):
|
||||
text = (f"Athena läuft im {mode['active'].upper()}-Modus. "
|
||||
f"Phase: {mode['phase']}. Musik-Worker: "
|
||||
f"{mode['music_worker']}. Stem-Separator: "
|
||||
f"{mode['separator_worker']}. LLM-Profil: {profile or 'entladen'}.")
|
||||
f"{mode['separator_worker']}. Voice Studio: "
|
||||
f"{mode['voice_worker']}. LLM-Profil: {profile or 'entladen'}.")
|
||||
else:
|
||||
target = ("music" if command == "/athena music" else
|
||||
"separation" if command in {"/athena stems", "/athena separation"}
|
||||
else "voice" if command == "/athena voice"
|
||||
else "llm")
|
||||
try:
|
||||
started, phase = schedule_operating_mode(target)
|
||||
@@ -2656,6 +2705,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if target == "music" else
|
||||
"Stimmtrennung wird gestartet. LLM und TTS werden entladen."
|
||||
if target == "separation" else
|
||||
"Voice Studio wird gestartet. LLM und TTS werden entladen."
|
||||
if target == "voice" else
|
||||
"Spezialmodus wird beendet und das vorherige LLM-Profil wiederhergestellt.")
|
||||
else:
|
||||
text = (f"Athena ist bereits im {target.upper()}-Modus "
|
||||
@@ -2948,15 +2999,14 @@ def _startup_reconcile() -> None:
|
||||
log.info("Startup-Retention: %d alte Bilder entfernt", len(removed))
|
||||
|
||||
special_mode = previous.get("mode")
|
||||
if ENABLE_MUSIC_MODE and special_mode in {"music", "separation"}:
|
||||
if ENABLE_MUSIC_MODE and special_mode in {"music", "separation", "voice"}:
|
||||
STATE.mode = special_mode
|
||||
STATE.mode_phase = f"starting-{special_mode}"
|
||||
_set_qwen_unavailable(True)
|
||||
try:
|
||||
path = ("/workers/music/start" if special_mode == "music"
|
||||
else "/workers/separator/start")
|
||||
path, _worker_state, wait_ready = _special_worker(special_mode)
|
||||
_profile_controller_request("POST", path)
|
||||
_wait_music_ready() if special_mode == "music" else _wait_separator_ready()
|
||||
wait_ready()
|
||||
STATE.mode_phase = "ready"
|
||||
RUNTIME.save(mode=special_mode, phase=special_mode)
|
||||
log.info("Recovery: Spezialmodus %s wiederhergestellt", special_mode)
|
||||
|
||||
Reference in New Issue
Block a user