From f2052adb78fd844074c39aa77f92d66eb1131d8e Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Thu, 10 Sep 2026 23:53:19 +0200 Subject: [PATCH] Integrate YuE2 as an exclusive Athena mode --- ATHENA.md | 4 + Erklärung.txt | 5 + README.md | 1 + compose.yaml | 6 + dev/test_dashboard_modes.py | 16 +++ dev/test_profile_controller.py | 29 +++++ docs/CONTAINER_INVENTORY.md | 1 + docs/OPERATING_MODES.md | 19 ++- experiments/yue2-3b/compose.yaml | 11 ++ for_ki.md | 8 ++ .../profile-controller/profile_controller.py | 123 ++++++++++++++++++ .../docker/wireguard-gateway/entrypoint.sh | 2 + platform/llama-dashboard/app.py | 15 ++- router/ai_profile_router.py | 53 +++++++- 14 files changed, 278 insertions(+), 15 deletions(-) diff --git a/ATHENA.md b/ATHENA.md index 35fb7a0..c6c120e 100644 --- a/ATHENA.md +++ b/ATHENA.md @@ -13,6 +13,7 @@ Sie betreibt: - FLUX.2 Klein 9B FP8 Beta für Textbilder und Referenzbild-Bearbeitung, - Qwen3-TTS für Sprache, - ACE-Step 1.5 XL-SFT als exklusiven Musikstudio-Modus, +- YuE2-3B mit Ladypoly-WebUI als zweiten, getrennten Musikstudio-Modus, - TRELLIS.2 4B Q8 als exklusives Bild-zu-3D-Studio, - das Athena-Dashboard, - Portainer CE als optionale Ansicht auf die laufenden Docker-Container, @@ -31,6 +32,7 @@ werden keine zweiten Instanzen dieser Dienste angelegt. | `/data/llama-dashboard` | historische Dashboard-Messwerte | | `/data/docker-backups` | automatische Athena-Backups | | `/data/trellis-studio` | trellis.cpp-Runtime und erzeugte 3D-Modelle | +| `/data/models/yue2`, `/data/music/yue2` | YuE2-Gewichte und dauerhafte Ergebnisse | | `/etc/mike-ai` | lokale Konfiguration und Secrets, niemals Git | Portainer läuft als separater, optionaler Verwaltungscontainer @@ -74,6 +76,8 @@ Qwen-Profil wird vom Profile Controller verwaltet. - ACE-Step 1.5 XL-SFT: exklusiver Musikmodus auf der RTX 5080. Dashboard und die Routerbefehle `/athena music`, `/athena llm`, `/athena status` bedienen dieselbe persistente Zustandsmaschine; siehe `docs/OPERATING_MODES.md`. +- YuE2-3B: eigener exklusiver Musikmodus mit Score- und Remix-Funktionen unter + `http://192.168.1.212:8014`. Der Routerbefehl lautet `/athena yue2`. - TRELLIS.2 4B Q8: exklusives Bild-zu-3D-Profil auf der RTX 5080. Die browserbasierte Oberfläche läuft unter `http://192.168.1.212:8013`, erzeugt GLB und verwendet standardmäßig `1024 · cascade`. Der 1536er Pfad kann die diff --git a/Erklärung.txt b/Erklärung.txt index a842a26..32f513c 100644 --- a/Erklärung.txt +++ b/Erklärung.txt @@ -93,6 +93,9 @@ Betriebsmodus gestartet. Zum Zeitpunkt der Aufnahme war Applio/RVC aktiv. | mike-ai-music-ui | läuft | Community-Weboberfläche für ACE-Step; das | | | | eigentliche Musikmodell wird separat geladen.| +-----------------------------------+-------------------+----------------------------------------------+ +| mike-ai-yue2-playground | gestoppt/bereit | Eigenständiges YuE2-Musikstudio mit Score-, | +| | | Generierungs- und Remix-Funktionen auf :8014.| ++-----------------------------------+-------------------+----------------------------------------------+ | mike-ai-stem-separator | gestoppt/bereit | Trennt Gesang, Begleitung und Instrumente | | | | mit BS-RoFormer und Demucs. | +-----------------------------------+-------------------+----------------------------------------------+ @@ -185,6 +188,8 @@ benötigten Modelle. +---------------+------------------------------------------------------------+ | Musik | ACE-Step 1.5 für Musikgenerierung. | +---------------+------------------------------------------------------------+ +| YuE2 Studio | YuE2-3B für Musik, Score-Steuerung und Audio-Remix. | ++---------------+------------------------------------------------------------+ | Audio trennen | BS-RoFormer, Demucs oder MossFormer2. | +---------------+------------------------------------------------------------+ | Voice Studio | OmniVoice für referenzbasierte Text-zu-Sprache-Ausgabe. | diff --git a/README.md b/README.md index 5fda660..0b50818 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ Details, Installation, Prüfung und Rollback stehen in - Applio (RVC-Inferenz, Modelle und Training): `http://192.168.1.212:8011` - Mikes Applio UI (geführte RVC-Oberfläche): `http://192.168.1.212:8012` - 3D Studio (TRELLIS.2 Q8, GLB-Ausgabe): `http://192.168.1.212:8013` +- YuE2 Studio (YuE2-3B, Generierung und Audio-Remix): `http://192.168.1.212:8014` Der Betriebsmodus lässt sich dort direkt umschalten. In Hermes funktionieren außerdem `/athena music`, `/athena stems`, `/athena voice`, diff --git a/compose.yaml b/compose.yaml index ed3f58e..8d390f9 100644 --- a/compose.yaml +++ b/compose.yaml @@ -487,10 +487,12 @@ services: RESTORE_WORKER: restore TTS_WORKER: qwen3 MUSIC_WORKER: acestep + YUE2_WORKER: yue2 SEPARATOR_WORKER: bs-roformer VOICE_WORKER: vevo2 VOICE_CHANGE_WORKER: xvc APPLIO_WORKER: applio + TRELLIS_WORKER: trellis2-q8 networks: [control] security_opt: ["no-new-privileges:true"] healthcheck: @@ -526,6 +528,8 @@ services: PROFILE_CONTROL_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}" SWITCH_TIMEOUT: "600" REQUEST_TIMEOUT: "600" + YUE2_START_TIMEOUT: "600" + TRELLIS_START_TIMEOUT: "900" # Last-resort guard for every OpenAI-compatible client. Without a # request limit llama.cpp uses n_predict=-1 and a reasoning loop can # consume the complete context before yielding visible output. @@ -748,6 +752,8 @@ services: VOICE_CHANGE_UI_URL: "${VOICE_CHANGE_UI_URL:-http://192.168.1.212:8009/}" APPLIO_UI_URL: "${APPLIO_UI_URL:-http://192.168.1.212:8011/}" MIKES_APPLIO_UI_URL: "${MIKES_APPLIO_UI_URL:-http://192.168.1.212:8012/}" + TRELLIS_UI_URL: "${TRELLIS_UI_URL:-http://192.168.1.212:8013/}" + YUE2_UI_URL: "${YUE2_UI_URL:-http://192.168.1.212:8014/}" HOST_PROC: /host/proc HOST_DATA: /host/data HOST_MODELS: /host/models diff --git a/dev/test_dashboard_modes.py b/dev/test_dashboard_modes.py index 08e92bd..0aaf214 100644 --- a/dev/test_dashboard_modes.py +++ b/dev/test_dashboard_modes.py @@ -80,6 +80,13 @@ class DashboardModeTests(unittest.TestCase): self.assertEqual(body, {"status": "accepted"}) self.assertEqual(json.loads(urlopen.call_args.args[0].data), {"mode": "applio"}) + def test_yue2_mode_is_forwarded_to_router(self): + with patch.object(self.dashboard.urllib.request, "urlopen", return_value=_Response()) as urlopen: + status, body = self.dashboard.change_mode("yue2") + self.assertEqual(status, 202) + self.assertEqual(body, {"status": "accepted"}) + self.assertEqual(json.loads(urlopen.call_args.args[0].data), {"mode": "yue2"}) + def test_unknown_mode_is_rejected_without_router_request(self): with patch.object(self.dashboard.urllib.request, "urlopen") as urlopen: status, body = self.dashboard.change_mode("unknown") @@ -104,6 +111,15 @@ class DashboardModeTests(unittest.TestCase): self.assertIn("http://192.168.1.212:8011/", html) self.assertIn("http://192.168.1.212:8012/", html) + def test_dashboard_keeps_ace_step_and_offers_yue2_separately(self): + html = self.dashboard.HTML + + self.assertIn("ACE-Step Studio", html) + self.assertIn("YuE2 Studio", html) + self.assertIn("setMode('music')", html) + self.assertIn("setMode('yue2')", html) + self.assertIn("http://192.168.1.212:8014/", html) + def test_dashboard_lists_only_portable_encrypted_backups(self): valid = self.backup_dir / "athena-portable-2026-09-10T10-00-00Z.tar.zst.age" valid.write_bytes(b"encrypted") diff --git a/dev/test_profile_controller.py b/dev/test_profile_controller.py index b9c6d00..5c515e5 100644 --- a/dev/test_profile_controller.py +++ b/dev/test_profile_controller.py @@ -41,6 +41,11 @@ def music_item(state="exited"): "Labels": {controller.MUSIC_LABEL_KEY: "acestep"}} +def yue2_item(state="exited"): + return {"Id": "id-yue2", "State": state, + "Labels": {controller.MUSIC_LABEL_KEY: "yue2"}} + + def separator_item(state="exited"): return {"Id": "id-separator", "State": state, "Labels": {controller.SEPARATOR_LABEL_KEY: "bs-roformer"}} @@ -57,6 +62,30 @@ def voice_change_item(state="exited"): class ProfileControllerTests(unittest.TestCase): + def test_yue2_start_exclusively_stops_llm_and_ace_step(self): + profiles = {name: item(name) for name in controller.ALLOWED} + profiles["ultra"] = item("ultra", "running") + calls = [] + + def request(method, path): + calls.append((method, path)) + return 204, b"" + + with patch.object(controller, "YUE2_WORKER", "yue2"), \ + patch.object(controller, "MUSIC_WORKER", "acestep"), \ + patch.object(controller, "containers", return_value=profiles), \ + patch.object(controller, "yue2_container", return_value=yue2_item()), \ + patch.object(controller, "music_container", return_value=music_item("running")), \ + patch.object(controller, "image_containers", return_value=[image_item("running")]), \ + patch.object(controller, "tts_container", return_value=tts_item()), \ + patch.object(controller, "docker_request", side_effect=request): + result = controller.set_yue2_worker(True) + + self.assertEqual(result, {"yue2_worker": "yue2", "state": "running"}) + self.assertIn(("POST", "/containers/id-ultra/stop?t=120"), calls) + self.assertIn(("POST", "/containers/id-music/stop?t=30"), calls) + self.assertEqual(calls[-1], ("POST", "/containers/id-yue2/start")) + def test_voice_change_start_exclusively_stops_gpu_workers(self): profiles = {name: item(name) for name in controller.ALLOWED} profiles["medium"] = item("medium", "running") diff --git a/docs/CONTAINER_INVENTORY.md b/docs/CONTAINER_INVENTORY.md index 09a6761..8917516 100644 --- a/docs/CONTAINER_INVENTORY.md +++ b/docs/CONTAINER_INVENTORY.md @@ -28,6 +28,7 @@ nicht automatisch ein ungenutzter Rest. | `mike-ai-router` | kein eigenes Modell | Einzige OpenAI-kompatible Modelladresse; koordiniert Profile, Bildaufträge, Sprache und Betriebsarten. | | `mike-ai-stem-separator` | BS-RoFormer Viperx 1297, `htdemucs_ft`, `htdemucs_6s`, `MossFormer2_SE_48K` | Trennt Gesang, Instrumente oder Sprache/Hintergrundgeräusche im exklusiven Separationsmodus. | | `mike-ai-trellis-studio` | TRELLIS.2 4B Q8 über trellis.cpp 0.6.0 | Erzeugt im exklusiven 3D-Modus aus einem Bild ein texturiertes, geschlossen aufbereitetes GLB-Mesh. Nutzt ausschließlich die RTX 5080 und wird über Port 8013 bedient. | +| `mike-ai-yue2-playground` | YuE2-3B mit Ladypoly `YuE2_WebUI` | Eigenständiges Musikstudio für Generierung, Score-basierte Steuerung und SheetSage2-Audioanalyse/Remix. Läuft exklusiv zu ACE-Step und allen übrigen GPU-Diensten; Zugriff über Port 8014. | | `mike-ai-tts-gateway` | kein eigenes Modell | Normalisiert Text, konvertiert Ausgabeformate und stellt Qwen3-TTS sowie natives PCM-Streaming über eine stabile interne API bereit. | | `mike-ai-voice-studio` | `k2-fsa/OmniVoice` 0.2.1 mit Whisper-ASR | Erzeugt Text-to-Speech mit einer Referenzstimme; kein Audio-to-Audio-Voice-Changer. | | `mike-ai-whisper` | Whisper.cpp `ggml-small` | Lokale deutsche Spracherkennung auf der CPU über `/v1/audio/transcriptions`. | diff --git a/docs/OPERATING_MODES.md b/docs/OPERATING_MODES.md index e918e0b..3a245f5 100644 --- a/docs/OPERATING_MODES.md +++ b/docs/OPERATING_MODES.md @@ -1,9 +1,11 @@ # Athena-Betriebsmodi -Athena besitzt sieben gegenseitig exklusive Betriebsmodi: +Athena besitzt acht 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. +- `yue2`: YuE2-3B und die angepasste `YuE2_WebUI` laufen; alle anderen + GPU-Dienste einschließlich ACE-Step sind gestoppt. - `separation`: BS-RoFormer und Demucs trennen Musikspuren; ClearVoice trennt Sprache von Hintergrundgeräuschen. LLM, Bild, TTS und ACE-Step sind gestoppt. - `voice`: OmniVoice erzeugt Sprache aus Text mit einer gewählten @@ -23,7 +25,7 @@ wird persistent gespeichert und beim Verlassen eines Spezialmodus wieder geladen ## Bedienung -Im Athena-Dashboard stehen **LLM-Betrieb**, **Musikstudio**, **Audio trennen**, +Im Athena-Dashboard stehen **LLM-Betrieb**, **ACE-Step Studio**, **YuE2 Studio**, **Audio trennen**, **Voice Studio**, **X-VC**, **Applio / RVC** und **3D Studio** bereit. Im Musikmodus werden zwei Oberflächen angeboten: - **Original UI · stabil** öffnet die zum laufenden ACE-Step-Image gehörende @@ -47,6 +49,15 @@ veröffentlicht. `ace-step-ui` ist reproduzierbar auf Commit `a1fdf91829ec6f7b98844f80e323529cd155dbf2` fixiert und greift intern über das Docker-Netz `mike-ai-music` auf `http://music-worker:7860` zu. +Das getrennte **YuE2 Studio** ist über WireGuard unter +`http://192.168.1.212:8014` erreichbar. Es verwendet YuE2-3B und die auf einen +festen Commit gesetzte `YuE2_WebUI` von Ladypoly. Analyse/Remix über +SheetSage2, Score-Übernahme und freie Generierung bleiben damit unabhängig vom +ACE-Step-Stack. Der Container trägt das Router-Label +`com.mike-ai.music-worker=yue2` und hängt als `yue2-studio` im privaten +Frontend-Netz. Ein Moduswechsel stoppt ihn zuverlässig, bevor LLM, +Audio-Trenner oder ein anderer GPU-Dienst gestartet werden. + 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. Gewählt wird die herauszulösende Quelle: Gesang, @@ -98,6 +109,7 @@ Router lokal beantwortet, auch wenn gerade kein LLM geladen ist: ```text /athena music +/athena yue2 /athena stems /athena voice /athena voicechange @@ -113,6 +125,7 @@ Die HTTP-Schnittstelle verwendet authentifizierte Requests: ```text GET /mode POST /mode {"mode":"music"} +POST /mode {"mode":"yue2"} POST /mode {"mode":"separation"} POST /mode {"mode":"voice"} POST /mode {"mode":"voicechange"} @@ -123,7 +136,7 @@ POST /mode {"mode":"llm"} Der Wechsel läuft asynchron. Fortschritt und Fehler stehen unter `mode` in `GET /status`. Der Profile-Controller akzeptiert ausschließlich den mit -`com.mike-ai.music-worker=acestep` beziehungsweise +`com.mike-ai.music-worker=acestep` oder `com.mike-ai.music-worker=yue2` beziehungsweise `com.mike-ai.stem-separator=bs-roformer` oder `com.mike-ai.voice-worker=vevo2` beziehungsweise `com.mike-ai.voice-change-worker=xvc` oder diff --git a/experiments/yue2-3b/compose.yaml b/experiments/yue2-3b/compose.yaml index d76a35b..0c60a2f 100644 --- a/experiments/yue2-3b/compose.yaml +++ b/experiments/yue2-3b/compose.yaml @@ -33,6 +33,8 @@ services: image: mike-ai/yue2:3b-0.1.6 container_name: mike-ai-yue2-playground restart: "no" + labels: + com.mike-ai.music-worker: yue2 entrypoint: ["python", "/opt/yue2/webui/server.py"] environment: NVIDIA_VISIBLE_DEVICES: "GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe" @@ -44,6 +46,9 @@ services: TRANSFORMERS_OFFLINE: "1" ports: - "127.0.0.1:8014:8014" + networks: + frontend: + aliases: [yue2-studio] volumes: - /data/models/yue2:/opt/yue2/models:ro - /data/music/yue2:/workspace/runs @@ -54,6 +59,7 @@ services: - driver: nvidia device_ids: ["GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"] capabilities: [gpu] + healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8014/api/state', timeout=2)"] interval: 5s @@ -91,3 +97,8 @@ services: - driver: nvidia device_ids: ["GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe"] capabilities: [gpu] + +networks: + frontend: + external: true + name: mike-ai_frontend diff --git a/for_ki.md b/for_ki.md index 6d33b5d..83d9103 100644 --- a/for_ki.md +++ b/for_ki.md @@ -100,12 +100,20 @@ Wechsel zu `llm` wird das zuvor gemerkte Profil wiederhergestellt. | `llm` | ein Qwen-Profil + Qwen3-TTS | profilabhängig beide GPUs; TTS RTX 3060 | Router `:8081` | | Bildauftrag | FLUX.2 Klein 9B FP8 + Qwen3-8B NF4 | RTX 5080 + RTX 3060, transaktional | über Router | | `music` | ACE-Step 1.5 XL-SFT | RTX 5080 | `:7862` original, `:7861` Community | +| `yue2` | YuE2-3B + Ladypoly `YuE2_WebUI` | RTX 5080 | `:8014` | | `separation` | BS-RoFormer, Demucs, MossFormer2 | RTX 5080 | `:8007` | | `voice` | OmniVoice | RTX 5080 | `:8008` | | `voicechange` | X-VC + optional Resemble Enhance | RTX 5080 | `:8009` | | `applio` | Applio/RVC | RTX 5080 | `:8011`, eigene UI `:8012` | | `trellis` | TRELLIS.2 4B Q8 über trellis.cpp 0.6.0 | ausschließlich RTX 5080 | `:8013` | +YuE2 liegt unter `/opt/mike-ai/yue2-3b`, seine Gewichte unter +`/data/models/yue2` und Ergebnisse unter `/data/music/yue2`. Der Container +trägt `com.mike-ai.music-worker=yue2` und muss mit dem Alias `yue2-studio` am +externen Netz `mike-ai_frontend` hängen. YuE2 niemals außerhalb der +Router-Zustandsmaschine dauerhaft starten: Sonst bleibt sein VRAM belegt und +der nächste LLM- oder Separator-Start kann mit OOM scheitern. + TRELLIS liegt unter `/opt/mike-ai/trellis-studio`. Seine Q8-Gewichte liegen unter `/data/models/trellis2-q8`, die Runtime und Ausgaben unter `/data/trellis-studio`. Die Oberfläche liefert GLB. `1024 · cascade` ist der diff --git a/platform/docker/profile-controller/profile_controller.py b/platform/docker/profile-controller/profile_controller.py index 2ff1de0..dbf4c6f 100644 --- a/platform/docker/profile-controller/profile_controller.py +++ b/platform/docker/profile-controller/profile_controller.py @@ -30,6 +30,7 @@ TTS_LABEL_KEY = "com.mike-ai.tts-worker" TTS_WORKER = os.environ.get("TTS_WORKER", "qwen3") MUSIC_LABEL_KEY = "com.mike-ai.music-worker" MUSIC_WORKER = os.environ.get("MUSIC_WORKER", "").strip() +YUE2_WORKER = os.environ.get("YUE2_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" @@ -38,6 +39,8 @@ VOICE_CHANGE_LABEL_KEY = "com.mike-ai.voice-change-worker" VOICE_CHANGE_WORKER = os.environ.get("VOICE_CHANGE_WORKER", "").strip() APPLIO_LABEL_KEY = "com.mike-ai.applio-worker" APPLIO_WORKER = os.environ.get("APPLIO_WORKER", "").strip() +TRELLIS_LABEL_KEY = "com.mike-ai.trellis-worker" +TRELLIS_WORKER = os.environ.get("TRELLIS_WORKER", "").strip() LOCK = threading.Lock() log = logging.getLogger("profile-controller") @@ -117,6 +120,17 @@ def music_container() -> dict: return matches[0] +def yue2_container() -> dict: + if not YUE2_WORKER: + raise RuntimeError("YuE2 worker is not configured") + matches = [item for item in labelled_containers(MUSIC_LABEL_KEY) + if item.get("Labels", {}).get(MUSIC_LABEL_KEY) == YUE2_WORKER] + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one YuE2 worker {YUE2_WORKER!r}, found {len(matches)}") + return matches[0] + + def separator_container() -> dict: if not SEPARATOR_WORKER: raise RuntimeError("stem separator is not configured") @@ -161,11 +175,27 @@ def applio_container() -> dict: return matches[0] +def trellis_container() -> dict: + if not TRELLIS_WORKER: + raise RuntimeError("TRELLIS worker is not configured") + matches = [item for item in labelled_containers(TRELLIS_LABEL_KEY) + if item.get("Labels", {}).get(TRELLIS_LABEL_KEY) == TRELLIS_WORKER] + if len(matches) != 1: + raise RuntimeError( + f"expected exactly one TRELLIS worker {TRELLIS_WORKER!r}, found {len(matches)}") + return matches[0] + + def stop_music_if_configured() -> None: if MUSIC_WORKER: stop_container(music_container(), timeout=30) +def stop_yue2_if_configured() -> None: + if YUE2_WORKER: + stop_container(yue2_container(), timeout=30) + + def stop_separator_if_configured() -> None: if SEPARATOR_WORKER: stop_container(separator_container(), timeout=30) @@ -186,6 +216,11 @@ def stop_applio_if_configured() -> None: stop_container(applio_container(), timeout=30) +def stop_trellis_if_configured() -> None: + if TRELLIS_WORKER: + stop_container(trellis_container(), timeout=30) + + def stop_voice_tools(except_kind: str | None = None) -> None: if except_kind != "voice": stop_voice_if_configured() @@ -233,8 +268,10 @@ def set_image_worker(running: bool, kind: str = IMAGE_WORKER) -> dict: # Qwen3-TTS. TTS is unavailable during this exclusive GPU phase. stop_container(tts_container(), timeout=30) stop_music_if_configured() + stop_yue2_if_configured() stop_separator_if_configured() stop_voice_tools() + stop_trellis_if_configured() for other in image_containers(): if other["Id"] != item["Id"]: stop_container(other, timeout=20) @@ -262,6 +299,8 @@ def set_music_worker(running: bool) -> dict: stop_container(tts_container(), timeout=30) stop_separator_if_configured() stop_voice_tools() + stop_yue2_if_configured() + stop_trellis_if_configured() start_container(item) else: stop_container(item, timeout=30) @@ -269,6 +308,27 @@ def set_music_worker(running: bool) -> dict: "state": "running" if running else "stopped"} +def set_yue2_worker(running: bool) -> dict: + """Start YuE2 exclusively, or stop it before another mode is loaded.""" + with LOCK: + item = yue2_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() + stop_trellis_if_configured() + start_container(item) + else: + stop_container(item, timeout=30) + return {"yue2_worker": YUE2_WORKER, + "state": "running" if running else "stopped"} + + def set_separator_worker(running: bool) -> dict: """Start vocal separation exclusively, or stop it before LLM restoration.""" with LOCK: @@ -280,7 +340,9 @@ def set_separator_worker(running: bool) -> dict: stop_container(worker, timeout=20) stop_container(tts_container(), timeout=30) stop_music_if_configured() + stop_yue2_if_configured() stop_voice_tools() + stop_trellis_if_configured() start_container(item) else: stop_container(item, timeout=30) @@ -299,8 +361,10 @@ def set_voice_worker(running: bool) -> dict: stop_container(worker, timeout=20) stop_container(tts_container(), timeout=30) stop_music_if_configured() + stop_yue2_if_configured() stop_separator_if_configured() stop_voice_tools("voice") + stop_trellis_if_configured() start_container(item) else: stop_container(item, timeout=30) @@ -319,8 +383,10 @@ def set_voice_change_worker(running: bool) -> dict: stop_container(worker, timeout=20) stop_container(tts_container(), timeout=30) stop_music_if_configured() + stop_yue2_if_configured() stop_separator_if_configured() stop_voice_tools("voicechange") + stop_trellis_if_configured() start_container(item) else: stop_container(item, timeout=30) @@ -339,8 +405,10 @@ def set_applio_worker(running: bool) -> dict: stop_container(worker, timeout=20) stop_container(tts_container(), timeout=30) stop_music_if_configured() + stop_yue2_if_configured() stop_separator_if_configured() stop_voice_tools("applio") + stop_trellis_if_configured() start_container(item) else: stop_container(item, timeout=30) @@ -348,6 +416,27 @@ def set_applio_worker(running: bool) -> dict: "state": "running" if running else "stopped"} +def set_trellis_worker(running: bool) -> dict: + """Start TRELLIS.2 exclusively, or stop it before another mode is loaded.""" + with LOCK: + item = trellis_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_yue2_if_configured() + stop_separator_if_configured() + stop_voice_tools() + start_container(item) + else: + stop_container(item, timeout=30) + return {"trellis_worker": TRELLIS_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"] @@ -364,8 +453,10 @@ def activate(profile: str) -> dict: for worker in image_containers(): stop_container(worker) stop_music_if_configured() + stop_yue2_if_configured() stop_separator_if_configured() stop_voice_tools() + stop_trellis_if_configured() start_container(tts_container()) items = containers() missing = [name for name in ALLOWED if name not in items] @@ -437,6 +528,13 @@ class Handler(BaseHTTPRequestHandler): "unhealthy" if "(unhealthy)" in music_status else "starting" if music.get("State") == "running" else "stopped") + yue2 = yue2_container() if YUE2_WORKER else {} + yue2_status = yue2.get("Status", "") + yue2_health = ("disabled" if not YUE2_WORKER else + "healthy" if "(healthy)" in yue2_status else + "unhealthy" if "(unhealthy)" in yue2_status else + "starting" if yue2.get("State") == "running" else + "stopped") separator = separator_container() if SEPARATOR_WORKER else {} separator_status = separator.get("Status", "") separator_health = ("disabled" if not SEPARATOR_WORKER else @@ -465,9 +563,18 @@ class Handler(BaseHTTPRequestHandler): "unhealthy" if "(unhealthy)" in applio_status else "starting" if applio.get("State") == "running" else "stopped") + trellis = trellis_container() if TRELLIS_WORKER else {} + trellis_status = trellis.get("Status", "") + trellis_health = ("disabled" if not TRELLIS_WORKER else + "healthy" if "(healthy)" in trellis_status else + "unhealthy" if "(unhealthy)" in trellis_status else + "starting" if trellis.get("State") == "running" else + "stopped") self.reply(200, {"active_profile": active_profile(items), "music_worker": music.get("State", "disabled"), "music_health": music_health, + "yue2_worker": yue2.get("State", "disabled"), + "yue2_health": yue2_health, "separator_worker": separator.get("State", "disabled"), "separator_health": separator_health, "voice_worker": voice.get("State", "disabled"), @@ -476,6 +583,8 @@ class Handler(BaseHTTPRequestHandler): "voice_change_health": voice_change_health, "applio_worker": applio.get("State", "disabled"), "applio_health": applio_health, + "trellis_worker": trellis.get("State", "disabled"), + "trellis_health": trellis_health, "profiles": {name: items.get(name, {}).get( "State", "missing") for name in ALLOWED}}) except Exception as exc: @@ -500,6 +609,13 @@ class Handler(BaseHTTPRequestHandler): log.exception("music worker transition failed") self.reply(503, {"error": str(exc)}) return + if self.path in {"/workers/yue2/start", "/workers/yue2/stop"}: + try: + self.reply(200, set_yue2_worker(self.path.endswith("/start"))) + except Exception as exc: + log.exception("YuE2 worker transition failed") + self.reply(503, {"error": str(exc)}) + return if self.path in {"/workers/separator/start", "/workers/separator/stop"}: try: self.reply(200, set_separator_worker(self.path.endswith("/start"))) @@ -528,6 +644,13 @@ class Handler(BaseHTTPRequestHandler): log.exception("Applio worker transition failed") self.reply(503, {"error": str(exc)}) return + if self.path in {"/workers/trellis/start", "/workers/trellis/stop"}: + try: + self.reply(200, set_trellis_worker(self.path.endswith("/start"))) + except Exception as exc: + log.exception("TRELLIS worker transition failed") + self.reply(503, {"error": str(exc)}) + return worker_paths = { "/workers/image/start": (IMAGE_WORKER, True), "/workers/image/stop": (IMAGE_WORKER, False), diff --git a/platform/docker/wireguard-gateway/entrypoint.sh b/platform/docker/wireguard-gateway/entrypoint.sh index 30e712e..34c2043 100644 --- a/platform/docker/wireguard-gateway/entrypoint.sh +++ b/platform/docker/wireguard-gateway/entrypoint.sh @@ -84,6 +84,8 @@ start_proxy 8008 voice-studio:8008 start_proxy 8009 xvc-studio:8009 start_proxy 8011 applio-studio:6969 start_proxy 8012 mikes-applio-ui:8012 +start_proxy 8013 trellis-studio:8080 +start_proxy 8014 yue2-studio:8014 start_proxy 8202 mcp-athena-operator:8000 start_proxy 9443 portainer:9443 diff --git a/platform/llama-dashboard/app.py b/platform/llama-dashboard/app.py index 6215a86..2f5a925 100644 --- a/platform/llama-dashboard/app.py +++ b/platform/llama-dashboard/app.py @@ -34,6 +34,8 @@ APPLIO_UI_URL = os.getenv("APPLIO_UI_URL", "http://192.168.1.212:8011/") MIKES_APPLIO_UI_URL = os.getenv( "MIKES_APPLIO_UI_URL", "http://192.168.1.212:8012/" ) +TRELLIS_UI_URL = os.getenv("TRELLIS_UI_URL", "http://192.168.1.212:8013/") +YUE2_UI_URL = os.getenv("YUE2_UI_URL", "http://192.168.1.212:8014/") HOST_PROC = Path(os.getenv("HOST_PROC", "/host/proc")) HOST_DATA = os.getenv("HOST_DATA", "/host/data") HOST_MODELS = Path(os.getenv("HOST_MODELS", "/host/models")) @@ -311,7 +313,8 @@ def router_status() -> tuple[dict[str, Any], str | None]: def change_mode(mode: str) -> tuple[int, dict[str, Any]]: - if mode not in {"llm", "music", "separation", "voice", "voicechange", "applio"}: + if mode not in {"llm", "music", "yue2", "separation", "voice", + "voicechange", "applio", "trellis"}: return 400, {"error": "invalid mode"} headers = {"Accept": "application/json", "Content-Type": "application/json"} if ROUTER_API_KEY: @@ -670,7 +673,7 @@ HTML = r'''