Add Seed-VC and Applio studio modes

This commit is contained in:
Mikei386
2026-09-09 20:58:32 +02:00
parent 51ed201f6c
commit 13a6714b2b
16 changed files with 410 additions and 33 deletions
@@ -36,6 +36,10 @@ VOICE_LABEL_KEY = "com.mike-ai.voice-worker"
VOICE_WORKER = os.environ.get("VOICE_WORKER", "").strip()
VOICE_CHANGE_LABEL_KEY = "com.mike-ai.voice-change-worker"
VOICE_CHANGE_WORKER = os.environ.get("VOICE_CHANGE_WORKER", "").strip()
SEED_VC_LABEL_KEY = "com.mike-ai.seed-vc-worker"
SEED_VC_WORKER = os.environ.get("SEED_VC_WORKER", "").strip()
APPLIO_LABEL_KEY = "com.mike-ai.applio-worker"
APPLIO_WORKER = os.environ.get("APPLIO_WORKER", "").strip()
LOCK = threading.Lock()
log = logging.getLogger("profile-controller")
@@ -148,6 +152,28 @@ def voice_change_container() -> dict:
return matches[0]
def seed_vc_container() -> dict:
if not SEED_VC_WORKER:
raise RuntimeError("Seed-VC worker is not configured")
matches = [item for item in labelled_containers(SEED_VC_LABEL_KEY)
if item.get("Labels", {}).get(SEED_VC_LABEL_KEY) == SEED_VC_WORKER]
if len(matches) != 1:
raise RuntimeError(
f"expected exactly one Seed-VC worker {SEED_VC_WORKER!r}, found {len(matches)}")
return matches[0]
def applio_container() -> dict:
if not APPLIO_WORKER:
raise RuntimeError("Applio worker is not configured")
matches = [item for item in labelled_containers(APPLIO_LABEL_KEY)
if item.get("Labels", {}).get(APPLIO_LABEL_KEY) == APPLIO_WORKER]
if len(matches) != 1:
raise RuntimeError(
f"expected exactly one Applio worker {APPLIO_WORKER!r}, found {len(matches)}")
return matches[0]
def stop_music_if_configured() -> None:
if MUSIC_WORKER:
stop_container(music_container(), timeout=30)
@@ -168,6 +194,27 @@ def stop_voice_change_if_configured() -> None:
stop_container(voice_change_container(), timeout=30)
def stop_seed_vc_if_configured() -> None:
if SEED_VC_WORKER:
stop_container(seed_vc_container(), timeout=30)
def stop_applio_if_configured() -> None:
if APPLIO_WORKER:
stop_container(applio_container(), timeout=30)
def stop_voice_tools(except_kind: str | None = None) -> None:
if except_kind != "voice":
stop_voice_if_configured()
if except_kind != "voicechange":
stop_voice_change_if_configured()
if except_kind != "seedvc":
stop_seed_vc_if_configured()
if except_kind != "applio":
stop_applio_if_configured()
def stop_container(item: dict, timeout: int = 120) -> None:
if item.get("State") != "running":
return
@@ -207,8 +254,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()
stop_voice_change_if_configured()
stop_voice_tools()
for other in image_containers():
if other["Id"] != item["Id"]:
stop_container(other, timeout=20)
@@ -235,8 +281,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()
stop_voice_change_if_configured()
stop_voice_tools()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -255,8 +300,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()
stop_voice_change_if_configured()
stop_voice_tools()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -276,7 +320,7 @@ def set_voice_worker(running: bool) -> dict:
stop_container(tts_container(), timeout=30)
stop_music_if_configured()
stop_separator_if_configured()
stop_voice_change_if_configured()
stop_voice_tools("voice")
start_container(item)
else:
stop_container(item, timeout=30)
@@ -296,7 +340,7 @@ def set_voice_change_worker(running: bool) -> dict:
stop_container(tts_container(), timeout=30)
stop_music_if_configured()
stop_separator_if_configured()
stop_voice_if_configured()
stop_voice_tools("voicechange")
start_container(item)
else:
stop_container(item, timeout=30)
@@ -304,6 +348,46 @@ def set_voice_change_worker(running: bool) -> dict:
"state": "running" if running else "stopped"}
def set_seed_vc_worker(running: bool) -> dict:
"""Start Seed-VC exclusively, or stop it before another mode is loaded."""
with LOCK:
item = seed_vc_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()
stop_voice_tools("seedvc")
start_container(item)
else:
stop_container(item, timeout=30)
return {"seed_vc_worker": SEED_VC_WORKER,
"state": "running" if running else "stopped"}
def set_applio_worker(running: bool) -> dict:
"""Start Applio exclusively, or stop it before another mode is loaded."""
with LOCK:
item = applio_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()
stop_voice_tools("applio")
start_container(item)
else:
stop_container(item, timeout=30)
return {"applio_worker": APPLIO_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"]
@@ -321,8 +405,7 @@ def activate(profile: str) -> dict:
stop_container(worker)
stop_music_if_configured()
stop_separator_if_configured()
stop_voice_if_configured()
stop_voice_change_if_configured()
stop_voice_tools()
start_container(tts_container())
items = containers()
missing = [name for name in ALLOWED if name not in items]
@@ -415,6 +498,20 @@ class Handler(BaseHTTPRequestHandler):
"unhealthy" if "(unhealthy)" in voice_change_status else
"starting" if voice_change.get("State") == "running" else
"stopped")
seed_vc = seed_vc_container() if SEED_VC_WORKER else {}
seed_vc_status = seed_vc.get("Status", "")
seed_vc_health = ("disabled" if not SEED_VC_WORKER else
"healthy" if "(healthy)" in seed_vc_status else
"unhealthy" if "(unhealthy)" in seed_vc_status else
"starting" if seed_vc.get("State") == "running" else
"stopped")
applio = applio_container() if APPLIO_WORKER else {}
applio_status = applio.get("Status", "")
applio_health = ("disabled" if not APPLIO_WORKER else
"healthy" if "(healthy)" in applio_status else
"unhealthy" if "(unhealthy)" in applio_status else
"starting" if applio.get("State") == "running" else
"stopped")
self.reply(200, {"active_profile": active_profile(items),
"music_worker": music.get("State", "disabled"),
"music_health": music_health,
@@ -424,6 +521,10 @@ class Handler(BaseHTTPRequestHandler):
"voice_health": voice_health,
"voice_change_worker": voice_change.get("State", "disabled"),
"voice_change_health": voice_change_health,
"seed_vc_worker": seed_vc.get("State", "disabled"),
"seed_vc_health": seed_vc_health,
"applio_worker": applio.get("State", "disabled"),
"applio_health": applio_health,
"profiles": {name: items.get(name, {}).get(
"State", "missing") for name in ALLOWED}})
except Exception as exc:
@@ -469,6 +570,20 @@ class Handler(BaseHTTPRequestHandler):
log.exception("voice-change worker transition failed")
self.reply(503, {"error": str(exc)})
return
if self.path in {"/workers/seed-vc/start", "/workers/seed-vc/stop"}:
try:
self.reply(200, set_seed_vc_worker(self.path.endswith("/start")))
except Exception as exc:
log.exception("Seed-VC worker transition failed")
self.reply(503, {"error": str(exc)})
return
if self.path in {"/workers/applio/start", "/workers/applio/stop"}:
try:
self.reply(200, set_applio_worker(self.path.endswith("/start")))
except Exception as exc:
log.exception("Applio worker transition failed")
self.reply(503, {"error": str(exc)})
return
worker_paths = {
"/workers/image/start": (IMAGE_WORKER, True),
"/workers/image/stop": (IMAGE_WORKER, False),
@@ -83,6 +83,8 @@ start_proxy 7862 music-worker:7860
start_proxy 8007 stem-separator:8080
start_proxy 8008 voice-studio:8008
start_proxy 8009 xvc-studio:8009
start_proxy 8010 seed-vc-studio:7860
start_proxy 8011 applio-studio:6969
start_proxy 8202 mcp-athena-operator:8000
start_proxy 9443 portainer:9443