Add private Vevo2 voice studio mode
This commit is contained in:
@@ -32,6 +32,8 @@ MUSIC_LABEL_KEY = "com.mike-ai.music-worker"
|
||||
MUSIC_WORKER = os.environ.get("MUSIC_WORKER", "").strip()
|
||||
SEPARATOR_LABEL_KEY = "com.mike-ai.stem-separator"
|
||||
SEPARATOR_WORKER = os.environ.get("SEPARATOR_WORKER", "").strip()
|
||||
VOICE_LABEL_KEY = "com.mike-ai.voice-worker"
|
||||
VOICE_WORKER = os.environ.get("VOICE_WORKER", "").strip()
|
||||
LOCK = threading.Lock()
|
||||
log = logging.getLogger("profile-controller")
|
||||
|
||||
@@ -122,6 +124,17 @@ def separator_container() -> dict:
|
||||
return matches[0]
|
||||
|
||||
|
||||
def voice_container() -> dict:
|
||||
if not VOICE_WORKER:
|
||||
raise RuntimeError("voice worker is not configured")
|
||||
matches = [item for item in labelled_containers(VOICE_LABEL_KEY)
|
||||
if item.get("Labels", {}).get(VOICE_LABEL_KEY) == VOICE_WORKER]
|
||||
if len(matches) != 1:
|
||||
raise RuntimeError(
|
||||
f"expected exactly one voice worker {VOICE_WORKER!r}, found {len(matches)}")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def stop_music_if_configured() -> None:
|
||||
if MUSIC_WORKER:
|
||||
stop_container(music_container(), timeout=30)
|
||||
@@ -132,6 +145,11 @@ def stop_separator_if_configured() -> None:
|
||||
stop_container(separator_container(), timeout=30)
|
||||
|
||||
|
||||
def stop_voice_if_configured() -> None:
|
||||
if VOICE_WORKER:
|
||||
stop_container(voice_container(), timeout=30)
|
||||
|
||||
|
||||
def stop_container(item: dict, timeout: int = 120) -> None:
|
||||
if item.get("State") != "running":
|
||||
return
|
||||
@@ -171,6 +189,7 @@ def set_image_worker(running: bool, kind: str = IMAGE_WORKER) -> dict:
|
||||
stop_container(tts_container(), timeout=30)
|
||||
stop_music_if_configured()
|
||||
stop_separator_if_configured()
|
||||
stop_voice_if_configured()
|
||||
for other in image_containers():
|
||||
if other["Id"] != item["Id"]:
|
||||
stop_container(other, timeout=20)
|
||||
@@ -197,6 +216,7 @@ def set_music_worker(running: bool) -> dict:
|
||||
stop_container(worker, timeout=20)
|
||||
stop_container(tts_container(), timeout=30)
|
||||
stop_separator_if_configured()
|
||||
stop_voice_if_configured()
|
||||
start_container(item)
|
||||
else:
|
||||
stop_container(item, timeout=30)
|
||||
@@ -215,6 +235,7 @@ def set_separator_worker(running: bool) -> dict:
|
||||
stop_container(worker, timeout=20)
|
||||
stop_container(tts_container(), timeout=30)
|
||||
stop_music_if_configured()
|
||||
stop_voice_if_configured()
|
||||
start_container(item)
|
||||
else:
|
||||
stop_container(item, timeout=30)
|
||||
@@ -222,6 +243,25 @@ def set_separator_worker(running: bool) -> dict:
|
||||
"state": "running" if running else "stopped"}
|
||||
|
||||
|
||||
def set_voice_worker(running: bool) -> dict:
|
||||
"""Start Vevo2 exclusively, or stop it before LLM restoration."""
|
||||
with LOCK:
|
||||
item = voice_container()
|
||||
if running:
|
||||
for profile_item in containers().values():
|
||||
stop_container(profile_item)
|
||||
for worker in image_containers():
|
||||
stop_container(worker, timeout=20)
|
||||
stop_container(tts_container(), timeout=30)
|
||||
stop_music_if_configured()
|
||||
stop_separator_if_configured()
|
||||
start_container(item)
|
||||
else:
|
||||
stop_container(item, timeout=30)
|
||||
return {"voice_worker": VOICE_WORKER,
|
||||
"state": "running" if running else "stopped"}
|
||||
|
||||
|
||||
def active_profile(items: dict[str, dict] | None = None) -> str | None:
|
||||
items = items or containers()
|
||||
active = [name for name, item in items.items() if item.get("State") == "running"]
|
||||
@@ -239,6 +279,7 @@ def activate(profile: str) -> dict:
|
||||
stop_container(worker)
|
||||
stop_music_if_configured()
|
||||
stop_separator_if_configured()
|
||||
stop_voice_if_configured()
|
||||
start_container(tts_container())
|
||||
items = containers()
|
||||
missing = [name for name in ALLOWED if name not in items]
|
||||
@@ -317,11 +358,20 @@ class Handler(BaseHTTPRequestHandler):
|
||||
"unhealthy" if "(unhealthy)" in separator_status else
|
||||
"starting" if separator.get("State") == "running" else
|
||||
"stopped")
|
||||
voice = voice_container() if VOICE_WORKER else {}
|
||||
voice_status = voice.get("Status", "")
|
||||
voice_health = ("disabled" if not VOICE_WORKER else
|
||||
"healthy" if "(healthy)" in voice_status else
|
||||
"unhealthy" if "(unhealthy)" in voice_status else
|
||||
"starting" if voice.get("State") == "running" else
|
||||
"stopped")
|
||||
self.reply(200, {"active_profile": active_profile(items),
|
||||
"music_worker": music.get("State", "disabled"),
|
||||
"music_health": music_health,
|
||||
"separator_worker": separator.get("State", "disabled"),
|
||||
"separator_health": separator_health,
|
||||
"voice_worker": voice.get("State", "disabled"),
|
||||
"voice_health": voice_health,
|
||||
"profiles": {name: items.get(name, {}).get(
|
||||
"State", "missing") for name in ALLOWED}})
|
||||
except Exception as exc:
|
||||
@@ -353,6 +403,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
log.exception("stem separator transition failed")
|
||||
self.reply(503, {"error": str(exc)})
|
||||
return
|
||||
if self.path in {"/workers/voice/start", "/workers/voice/stop"}:
|
||||
try:
|
||||
self.reply(200, set_voice_worker(self.path.endswith("/start")))
|
||||
except Exception as exc:
|
||||
log.exception("voice worker transition failed")
|
||||
self.reply(503, {"error": str(exc)})
|
||||
return
|
||||
worker_paths = {
|
||||
"/workers/image/start": (IMAGE_WORKER, True),
|
||||
"/workers/image/stop": (IMAGE_WORKER, False),
|
||||
|
||||
@@ -81,6 +81,7 @@ start_proxy 8099 llama-dashboard:8099
|
||||
start_proxy 7861 music-ui:3000
|
||||
start_proxy 7862 music-worker:7860
|
||||
start_proxy 8007 stem-separator:8080
|
||||
start_proxy 8008 voice-studio:8008
|
||||
start_proxy 8202 mcp-athena-operator:8000
|
||||
start_proxy 9443 portainer:9443
|
||||
|
||||
|
||||
Reference in New Issue
Block a user