Add exclusive LTX-2 video studio profile

This commit is contained in:
Mikei386
2026-09-13 21:33:57 +02:00
parent fce9900389
commit a78aed8e9e
12 changed files with 233 additions and 15 deletions
+3
View File
@@ -498,6 +498,7 @@ services:
VOICE_CHANGE_WORKER: xvc VOICE_CHANGE_WORKER: xvc
APPLIO_WORKER: applio APPLIO_WORKER: applio
TRELLIS_WORKER: trellis2-q8 TRELLIS_WORKER: trellis2-q8
VIDEO_WORKER: ltx2
networks: [control] networks: [control]
security_opt: ["no-new-privileges:true"] security_opt: ["no-new-privileges:true"]
healthcheck: healthcheck:
@@ -535,6 +536,7 @@ services:
REQUEST_TIMEOUT: "600" REQUEST_TIMEOUT: "600"
YUE2_START_TIMEOUT: "600" YUE2_START_TIMEOUT: "600"
TRELLIS_START_TIMEOUT: "900" TRELLIS_START_TIMEOUT: "900"
VIDEO_START_TIMEOUT: "900"
# Last-resort guard for every OpenAI-compatible client. Without a # Last-resort guard for every OpenAI-compatible client. Without a
# request limit llama.cpp uses n_predict=-1 and a reasoning loop can # request limit llama.cpp uses n_predict=-1 and a reasoning loop can
# consume the complete context before yielding visible output. # consume the complete context before yielding visible output.
@@ -759,6 +761,7 @@ services:
MIKES_APPLIO_UI_URL: "${MIKES_APPLIO_UI_URL:-http://192.168.1.212:8012/}" 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/}" TRELLIS_UI_URL: "${TRELLIS_UI_URL:-http://192.168.1.212:8013/}"
YUE2_UI_URL: "${YUE2_UI_URL:-http://192.168.1.212:8014/}" YUE2_UI_URL: "${YUE2_UI_URL:-http://192.168.1.212:8014/}"
LTX2_UI_URL: "${LTX2_UI_URL:-http://192.168.1.212:8015/}"
HOST_PROC: /host/proc HOST_PROC: /host/proc
HOST_DATA: /host/data HOST_DATA: /host/data
HOST_MODELS: /host/models HOST_MODELS: /host/models
+16 -3
View File
@@ -1,6 +1,6 @@
# Athena-Betriebsmodi # Athena-Betriebsmodi
Athena besitzt sechs gegenseitig exklusive Betriebsmodi: Athena besitzt gegenseitig exklusive Betriebsmodi:
- `llm`: ein llama.cpp-Profil und Qwen3-TTS laufen; Spezialdienste sind gestoppt. - `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. - `music`: ACE-Step 1.5 XL-SFT läuft; alle LLM-, Bild-, TTS- und Separator-Worker sind gestoppt.
@@ -13,6 +13,8 @@ Athena besitzt sechs gegenseitig exklusive Betriebsmodi:
GPU-Dienste sind gestoppt. GPU-Dienste sind gestoppt.
- `applio`: Applio stellt RVC-Inferenz, Modellverwaltung und Training bereit. - `applio`: Applio stellt RVC-Inferenz, Modellverwaltung und Training bereit.
Alle anderen GPU-Dienste sind gestoppt. Alle anderen GPU-Dienste sind gestoppt.
- `video`: LTX Desktop erzeugt mit LTX-2 kurze Videos. Beim Start werden alle
LLM-, Bild-, TTS-, Musik-, Sprach-, RVC- und 3D-GPU-Dienste gestoppt.
Die Zustandsmaschine lebt im Athena-Router. Das Dashboard und Chat-Clients wie Die Zustandsmaschine lebt im Athena-Router. Das Dashboard und Chat-Clients wie
Hermes sind nur Bedienoberflächen derselben API. Der zuletzt aktive LLM-Modus Hermes sind nur Bedienoberflächen derselben API. Der zuletzt aktive LLM-Modus
@@ -21,7 +23,7 @@ wird persistent gespeichert und beim Verlassen eines Spezialmodus wieder geladen
## Bedienung ## Bedienung
Im Athena-Dashboard stehen **LLM-Betrieb**, **Musikstudio**, **Audio trennen**, Im Athena-Dashboard stehen **LLM-Betrieb**, **Musikstudio**, **Audio trennen**,
**Voice Studio**, **X-VC** und **Applio / RVC** bereit. Im Musikmodus werden zwei Oberflächen angeboten: **Voice Studio**, **X-VC**, **Applio / RVC**, **3D Studio** und **LTX-2 Video** bereit. Im Musikmodus werden zwei Oberflächen angeboten:
- **Original UI · stabil** öffnet die zum laufenden ACE-Step-Image gehörende - **Original UI · stabil** öffnet die zum laufenden ACE-Step-Image gehörende
Gradio-Oberfläche. Sie ist für Cover, Remix und erweiterte Workflows der Gradio-Oberfläche. Sie ist für Cover, Remix und erweiterte Workflows der
@@ -55,6 +57,14 @@ nicht eine reine Synthesizer-Spur. Sprache nutzt das 48-kHz-Modell
`audio-separator` 0.47.0. Die ältere API-Auswahl kompletter 2-/4-/6-Stem-Sätze `audio-separator` 0.47.0. Die ältere API-Auswahl kompletter 2-/4-/6-Stem-Sätze
bleibt rückwärtskompatibel. bleibt rückwärtskompatibel.
Das LTX-2 Studio ist ausschließlich unter `http://192.168.1.212:8015`
erreichbar. Es verwendet das offizielle LTX Desktop 1.2.7 und speichert Modelle,
Einstellungen und Ergebnisse unter `/data/video/ltx-desktop`. Die RTX 5080 liegt
mit 16 GiB am offiziellen Minimum. Daher ist LTX Fast mit höchstens etwa zehn
Sekunden und 720p oder kleiner der Startpunkt; längere oder größere Läufe sind
nicht zugesichert. Der erste Modelldownload kann eine Hugging-Face-Anmeldung und
die Annahme der Lightricks-Modelllizenz verlangen.
Das Voice Studio ist ausschließlich über den privaten WireGuard-Pfad unter Das Voice Studio ist ausschließlich über den privaten WireGuard-Pfad unter
`http://192.168.1.212:8008` erreichbar. Referenzstimmen werden unter `http://192.168.1.212:8008` erreichbar. Referenzstimmen werden unter
`/data/voice/studio/profiles` gespeichert. Die Oberfläche verlangt vor dem `/data/voice/studio/profiles` gespeichert. Die Oberfläche verlangt vor dem
@@ -89,6 +99,7 @@ Router lokal beantwortet, auch wenn gerade kein LLM geladen ist:
/athena voice /athena voice
/athena voicechange /athena voicechange
/athena applio /athena applio
/athena ltx2
/athena llm /athena llm
/athena status /athena status
``` ```
@@ -102,6 +113,7 @@ POST /mode {"mode":"separation"}
POST /mode {"mode":"voice"} POST /mode {"mode":"voice"}
POST /mode {"mode":"voicechange"} POST /mode {"mode":"voicechange"}
POST /mode {"mode":"applio"} POST /mode {"mode":"applio"}
POST /mode {"mode":"video"}
POST /mode {"mode":"llm"} POST /mode {"mode":"llm"}
``` ```
@@ -111,7 +123,8 @@ Der Wechsel läuft asynchron. Fortschritt und Fehler stehen unter `mode` in
`com.mike-ai.stem-separator=bs-roformer` oder `com.mike-ai.stem-separator=bs-roformer` oder
`com.mike-ai.voice-worker=vevo2` beziehungsweise `com.mike-ai.voice-worker=vevo2` beziehungsweise
`com.mike-ai.voice-change-worker=xvc` oder `com.mike-ai.voice-change-worker=xvc` oder
`com.mike-ai.applio-worker=applio` markierten Container; freie `com.mike-ai.applio-worker=applio` beziehungsweise
`com.mike-ai.video-worker=ltx2` markierten Container; freie
Container- oder Docker-Befehle werden nicht entgegengenommen. Container- oder Docker-Befehle werden nicht entgegengenommen.
## Wiederanlauf ## Wiederanlauf
@@ -42,6 +42,8 @@ APPLIO_LABEL_KEY = "com.mike-ai.applio-worker"
APPLIO_WORKER = os.environ.get("APPLIO_WORKER", "").strip() APPLIO_WORKER = os.environ.get("APPLIO_WORKER", "").strip()
TRELLIS_LABEL_KEY = "com.mike-ai.trellis-worker" TRELLIS_LABEL_KEY = "com.mike-ai.trellis-worker"
TRELLIS_WORKER = os.environ.get("TRELLIS_WORKER", "").strip() TRELLIS_WORKER = os.environ.get("TRELLIS_WORKER", "").strip()
VIDEO_LABEL_KEY = "com.mike-ai.video-worker"
VIDEO_WORKER = os.environ.get("VIDEO_WORKER", "").strip()
LOCK = threading.Lock() LOCK = threading.Lock()
log = logging.getLogger("profile-controller") log = logging.getLogger("profile-controller")
@@ -187,6 +189,22 @@ def trellis_container() -> dict:
return matches[0] return matches[0]
def video_container() -> dict:
if not VIDEO_WORKER:
raise RuntimeError("video worker is not configured")
matches = [item for item in labelled_containers(VIDEO_LABEL_KEY)
if item.get("Labels", {}).get(VIDEO_LABEL_KEY) == VIDEO_WORKER]
if len(matches) != 1:
raise RuntimeError(
f"expected exactly one video worker {VIDEO_WORKER!r}, found {len(matches)}")
return matches[0]
def stop_video_if_configured() -> None:
if VIDEO_WORKER:
stop_container(video_container(), timeout=30)
def stop_music_if_configured() -> None: def stop_music_if_configured() -> None:
if MUSIC_WORKER: if MUSIC_WORKER:
stop_container(music_container(), timeout=30) stop_container(music_container(), timeout=30)
@@ -290,6 +308,7 @@ def set_image_worker(running: bool, kind: str = IMAGE_WORKER) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools() stop_voice_tools()
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
for other in image_containers(): for other in image_containers():
if other["Id"] != item["Id"]: if other["Id"] != item["Id"]:
stop_container(other, timeout=20) stop_container(other, timeout=20)
@@ -319,6 +338,7 @@ def set_music_worker(running: bool) -> dict:
stop_voice_tools() stop_voice_tools()
stop_yue2_if_configured() stop_yue2_if_configured()
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -340,6 +360,7 @@ def set_yue2_worker(running: bool) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools() stop_voice_tools()
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -361,6 +382,7 @@ def set_separator_worker(running: bool) -> dict:
stop_yue2_if_configured() stop_yue2_if_configured()
stop_voice_tools() stop_voice_tools()
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -383,6 +405,7 @@ def set_voice_worker(running: bool) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools("voice") stop_voice_tools("voice")
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -405,6 +428,7 @@ def set_voice_change_worker(running: bool) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools("voicechange") stop_voice_tools("voicechange")
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -427,6 +451,7 @@ def set_applio_worker(running: bool) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools("applio") stop_voice_tools("applio")
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(item) start_container(item)
else: else:
stop_container(item, timeout=30) stop_container(item, timeout=30)
@@ -455,6 +480,28 @@ def set_trellis_worker(running: bool) -> dict:
"state": "running" if running else "stopped"} "state": "running" if running else "stopped"}
def set_video_worker(running: bool) -> dict:
"""Start LTX-2 exclusively, or stop it before another mode is loaded."""
with LOCK:
item = video_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()
stop_trellis_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
return {"video_worker": VIDEO_WORKER,
"state": "running" if running else "stopped"}
def active_profile(items: dict[str, dict] | None = None) -> str | None: def active_profile(items: dict[str, dict] | None = None) -> str | None:
items = items or containers() items = items or containers()
active = [name for name, item in items.items() if item.get("State") == "running"] active = [name for name, item in items.items() if item.get("State") == "running"]
@@ -475,6 +522,7 @@ def activate(profile: str) -> dict:
stop_separator_if_configured() stop_separator_if_configured()
stop_voice_tools() stop_voice_tools()
stop_trellis_if_configured() stop_trellis_if_configured()
stop_video_if_configured()
start_container(tts_container()) start_container(tts_container())
items = containers() items = containers()
missing = [name for name in ALLOWED if name not in items] missing = [name for name in ALLOWED if name not in items]
@@ -588,6 +636,13 @@ class Handler(BaseHTTPRequestHandler):
"unhealthy" if "(unhealthy)" in trellis_status else "unhealthy" if "(unhealthy)" in trellis_status else
"starting" if trellis.get("State") == "running" else "starting" if trellis.get("State") == "running" else
"stopped") "stopped")
video = video_container() if VIDEO_WORKER else {}
video_status = video.get("Status", "")
video_health = ("disabled" if not VIDEO_WORKER else
"healthy" if "(healthy)" in video_status else
"unhealthy" if "(unhealthy)" in video_status else
"starting" if video.get("State") == "running" else
"stopped")
self.reply(200, {"active_profile": active_profile(items), self.reply(200, {"active_profile": active_profile(items),
"music_worker": music.get("State", "disabled"), "music_worker": music.get("State", "disabled"),
"music_health": music_health, "music_health": music_health,
@@ -603,6 +658,8 @@ class Handler(BaseHTTPRequestHandler):
"applio_health": applio_health, "applio_health": applio_health,
"trellis_worker": trellis.get("State", "disabled"), "trellis_worker": trellis.get("State", "disabled"),
"trellis_health": trellis_health, "trellis_health": trellis_health,
"video_worker": video.get("State", "disabled"),
"video_health": video_health,
"profiles": {name: items.get(name, {}).get( "profiles": {name: items.get(name, {}).get(
"State", "missing") for name in ALLOWED}}) "State", "missing") for name in ALLOWED}})
except Exception as exc: except Exception as exc:
@@ -669,6 +726,13 @@ class Handler(BaseHTTPRequestHandler):
log.exception("TRELLIS worker transition failed") log.exception("TRELLIS worker transition failed")
self.reply(503, {"error": str(exc)}) self.reply(503, {"error": str(exc)})
return return
if self.path in {"/workers/video/start", "/workers/video/stop"}:
try:
self.reply(200, set_video_worker(self.path.endswith("/start")))
except Exception as exc:
log.exception("video worker transition failed")
self.reply(503, {"error": str(exc)})
return
worker_paths = { worker_paths = {
"/workers/image/start": (IMAGE_WORKER, True), "/workers/image/start": (IMAGE_WORKER, True),
"/workers/image/stop": (IMAGE_WORKER, False), "/workers/image/stop": (IMAGE_WORKER, False),
@@ -86,6 +86,7 @@ start_proxy 8011 applio-studio:6969
start_proxy 8012 mikes-applio-ui:8012 start_proxy 8012 mikes-applio-ui:8012
start_proxy 8013 trellis-studio:8080 start_proxy 8013 trellis-studio:8080
start_proxy 8014 yue2-studio:8014 start_proxy 8014 yue2-studio:8014
start_proxy 8015 ltx2-studio:8015
start_proxy 8202 mcp-athena-operator:8000 start_proxy 8202 mcp-athena-operator:8000
start_proxy 9443 portainer:9443 start_proxy 9443 portainer:9443
@@ -18,7 +18,7 @@ correct the durable source when the user requested maintenance.
## Exclusive states ## Exclusive states
Athena has five mutually exclusive persistent modes: `llm`, `music`, Athena has mutually exclusive persistent modes: `llm`, `music`,
`separation`, `voice`, and `voicechange`. Image generation is a transactional `separation`, `voice`, and `voicechange`. Image generation is a transactional
request: it temporarily pauses the active text profile and Qwen3-TTS, runs the request: it temporarily pauses the active text profile and Qwen3-TTS, runs the
image worker, then restores the previous LLM state. image worker, then restores the previous LLM state.
@@ -45,3 +45,6 @@ The GPU workers use `restart: "no"` and are created once, then started on
demand. A stopped `mike-ai-llama-*`, image, music, separator, OmniVoice or X-VC demand. A stopped `mike-ai-llama-*`, image, music, separator, OmniVoice or X-VC
container is expected. A candidate is stale only after checking Compose, container is expected. A candidate is stale only after checking Compose,
labels, mounts, router/controller references, model paths and test history. labels, mounts, router/controller references, model paths and test history.
`video` starts the allowlisted LTX Desktop worker exclusively. It is controlled with `/athena ltx2` and its private UI is exposed at `http://192.168.1.212:8015`.
File diff suppressed because one or more lines are too long
+34
View File
@@ -0,0 +1,34 @@
FROM ubuntu:24.04
ARG LTX_DESKTOP_VERSION=1.2.7
ARG LTX_DESKTOP_SHA512=d1d59027988a48490492feb42156665bbed511f187739a664ad326492fd8fc0ce43429537150eb6aea9a75a522f6353a40839f9b3ed0449fb720b7c13b091706
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl dbus-x11 ffmpeg libasound2t64 libatk-bridge2.0-0 \
libatk1.0-0 libcups2 libdrm2 libgbm1 libgtk-3-0 libnss3 libx11-xcb1 \
libxcomposite1 libxdamage1 libxfixes3 libxkbcommon0 libxrandr2 \
novnc openbox procps python3-websockify x11vnc xvfb \
&& rm -rf /var/lib/apt/lists/* \
&& install -d /opt/ltx-desktop \
&& curl -fL --retry 5 \
"https://github.com/Lightricks/LTX-Desktop/releases/download/v${LTX_DESKTOP_VERSION}/LTX-Desktop-x86_64.AppImage" \
-o /tmp/ltx-desktop.AppImage \
&& printf '%s %s\n' "$LTX_DESKTOP_SHA512" /tmp/ltx-desktop.AppImage | sha512sum -c - \
&& chmod +x /tmp/ltx-desktop.AppImage \
&& cd /opt/ltx-desktop \
&& /tmp/ltx-desktop.AppImage --appimage-extract >/dev/null \
&& rm /tmp/ltx-desktop.AppImage \
&& ln -s /usr/share/novnc/vnc.html /usr/share/novnc/index.html
COPY entrypoint.sh /usr/local/bin/ltx-desktop-entrypoint
RUN chmod 0755 /usr/local/bin/ltx-desktop-entrypoint
ENV DISPLAY=:0 \
HOME=/data/home \
XDG_DATA_HOME=/data \
XDG_CONFIG_HOME=/data/config \
XDG_CACHE_HOME=/data/cache \
NO_AT_BRIDGE=1
EXPOSE 8015
ENTRYPOINT ["/usr/local/bin/ltx-desktop-entrypoint"]
+16
View File
@@ -0,0 +1,16 @@
# LTX-2 Studio
This specialist profile runs the official LTX Desktop 1.2.7 application in a
browser-accessible private desktop. The image is pinned to the release checksum.
Application data, downloaded models and outputs persist below
`/data/video/ltx-desktop`.
The profile controller starts only the container labelled
`com.mike-ai.video-worker=ltx2`. Starting it stops every LLM, image, speech,
music, voice and 3D GPU worker first. The UI is reachable only through Athena's
WireGuard gateway at `http://192.168.1.212:8015`.
The RTX 5080 has the official minimum of 16 GiB VRAM for LTX Desktop local
generation. Start with LTX Fast, 720p or below and at most about ten seconds.
Model downloads may require accepting Lightricks' model license and signing in
to Hugging Face in the application.
+35
View File
@@ -0,0 +1,35 @@
services:
ltx2-studio:
build:
context: .
args:
LTX_DESKTOP_VERSION: "1.2.7"
LTX_DESKTOP_SHA512: "d1d59027988a48490492feb42156665bbed511f187739a664ad326492fd8fc0ce43429537150eb6aea9a75a522f6353a40839f9b3ed0449fb720b7c13b091706"
image: mike-ai/ltx2-studio:1.2.7
container_name: mike-ai-ltx2-studio
restart: "no"
labels:
com.mike-ai.video-worker: ltx2
gpus: all
shm_size: 8g
environment:
NVIDIA_VISIBLE_DEVICES: ${LTX2_GPU_UUID:-GPU-8ad38c6c-5a01-9d8e-1dfa-ed662ad78fbe}
NVIDIA_DRIVER_CAPABILITIES: compute,utility,graphics
volumes:
- /data/video/ltx-desktop:/data
networks:
- frontend
security_opt:
- no-new-privileges:true
cap_drop: [ALL]
healthcheck:
test: [CMD, curl, -fsS, http://127.0.0.1:8015/]
interval: 10s
timeout: 5s
retries: 30
start_period: 30s
networks:
frontend:
external: true
name: mike-ai_frontend
+30
View File
@@ -0,0 +1,30 @@
#!/bin/sh
set -eu
install -d -m 0755 "$HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" /data/models /data/outputs
cleanup() {
kill "${app_pid:-}" "${web_pid:-}" "${vnc_pid:-}" "${wm_pid:-}" "${x_pid:-}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM
Xvfb :0 -screen 0 1600x1000x24 -nolisten tcp &
x_pid=$!
for _ in $(seq 1 50); do
[ -S /tmp/.X11-unix/X0 ] && break
sleep 0.1
done
openbox >/tmp/openbox.log 2>&1 &
wm_pid=$!
x11vnc -display :0 -forever -shared -nopw -rfbport 5900 \
>/tmp/x11vnc.log 2>&1 &
vnc_pid=$!
websockify --web=/usr/share/novnc 8015 127.0.0.1:5900 \
>/tmp/websockify.log 2>&1 &
web_pid=$!
/opt/ltx-desktop/squashfs-root/AppRun --no-sandbox --disable-gpu-sandbox \
--disable-dev-shm-usage >/data/ltx-desktop.log 2>&1 &
app_pid=$!
wait "$app_pid"
+2
View File
@@ -14,6 +14,7 @@ fi
export VOICE_GPU_UUID=${VOICE_GPU_UUID:-${IMAGE_GPU_DEVICES:-}} export VOICE_GPU_UUID=${VOICE_GPU_UUID:-${IMAGE_GPU_DEVICES:-}}
export ACESTEP_GPU_UUID=${ACESTEP_GPU_UUID:-${IMAGE_GPU_DEVICES:-}} export ACESTEP_GPU_UUID=${ACESTEP_GPU_UUID:-${IMAGE_GPU_DEVICES:-}}
export SEPARATOR_GPU_UUID=${SEPARATOR_GPU_UUID:-${IMAGE_GPU_DEVICES:-}} export SEPARATOR_GPU_UUID=${SEPARATOR_GPU_UUID:-${IMAGE_GPU_DEVICES:-}}
export LTX2_GPU_UUID=${LTX2_GPU_UUID:-${IMAGE_GPU_DEVICES:-}}
create_project() { create_project() {
local dir=$1 file=${2:-compose.yaml} profile=${3:-} local dir=$1 file=${2:-compose.yaml} profile=${3:-}
@@ -32,5 +33,6 @@ create_project /opt/mike-ai/omnivoice-studio
create_project /opt/mike-ai/xvc-studio create_project /opt/mike-ai/xvc-studio
create_project /opt/mike-ai/stack/experiments/applio-rvc create_project /opt/mike-ai/stack/experiments/applio-rvc
create_project /opt/mike-ai/Mikes-Applio-UI compose.example.yaml create_project /opt/mike-ai/Mikes-Applio-UI compose.example.yaml
create_project /opt/mike-ai/ltx2-studio
printf 'ATHENA_SPECIALISTS_REBUILT_OK\n' printf 'ATHENA_SPECIALISTS_REBUILT_OK\n'
+21 -6
View File
@@ -114,6 +114,7 @@ VOICE_START_TIMEOUT = float(os.environ.get("VOICE_START_TIMEOUT", "600"))
VOICE_CHANGE_START_TIMEOUT = float(os.environ.get("VOICE_CHANGE_START_TIMEOUT", "600")) VOICE_CHANGE_START_TIMEOUT = float(os.environ.get("VOICE_CHANGE_START_TIMEOUT", "600"))
APPLIO_START_TIMEOUT = float(os.environ.get("APPLIO_START_TIMEOUT", "900")) APPLIO_START_TIMEOUT = float(os.environ.get("APPLIO_START_TIMEOUT", "900"))
TRELLIS_START_TIMEOUT = float(os.environ.get("TRELLIS_START_TIMEOUT", "900")) TRELLIS_START_TIMEOUT = float(os.environ.get("TRELLIS_START_TIMEOUT", "900"))
VIDEO_START_TIMEOUT = float(os.environ.get("VIDEO_START_TIMEOUT", "900"))
# Optional worker APIs. The clean Docker baseline deliberately ships only # Optional worker APIs. The clean Docker baseline deliberately ships only
# text/multimodal chat; absent workers must fail explicitly instead of trying # text/multimodal chat; absent workers must fail explicitly instead of trying
@@ -502,6 +503,11 @@ def _wait_trellis_ready() -> None:
"TRELLIS.2", TRELLIS_START_TIMEOUT) "TRELLIS.2", TRELLIS_START_TIMEOUT)
def _wait_video_ready() -> None:
_wait_aux_voice_ready("video_worker", "video_health",
"LTX-2 Studio", VIDEO_START_TIMEOUT)
def _special_worker(mode: str) -> tuple[str, str, callable]: def _special_worker(mode: str) -> tuple[str, str, callable]:
if mode == "music": if mode == "music":
return "/workers/music/start", _music_worker_state(), _wait_music_ready return "/workers/music/start", _music_worker_state(), _wait_music_ready
@@ -521,6 +527,9 @@ def _special_worker(mode: str) -> tuple[str, str, callable]:
if mode == "trellis": if mode == "trellis":
return ("/workers/trellis/start", _worker_field("trellis_worker"), return ("/workers/trellis/start", _worker_field("trellis_worker"),
_wait_trellis_ready) _wait_trellis_ready)
if mode == "video":
return ("/workers/video/start", _worker_field("video_worker"),
_wait_video_ready)
raise ValueError(f"unbekannter Spezialmodus: {mode}") raise ValueError(f"unbekannter Spezialmodus: {mode}")
@@ -529,7 +538,7 @@ def set_operating_mode(mode: str) -> dict:
if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL: if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL:
raise RuntimeError("Musikmodus ist nicht konfiguriert") raise RuntimeError("Musikmodus ist nicht konfiguriert")
special_modes = {"music", "yue2", "separation", "voice", "voicechange", special_modes = {"music", "yue2", "separation", "voice", "voicechange",
"applio", "trellis"} "applio", "trellis", "video"}
if mode not in {"llm", *special_modes}: if mode not in {"llm", *special_modes}:
raise ValueError("unbekannter Betriebsmodus") raise ValueError("unbekannter Betriebsmodus")
with STATE.lock: with STATE.lock:
@@ -582,6 +591,7 @@ def set_operating_mode(mode: str) -> dict:
_profile_controller_request("POST", "/workers/voice-change/stop") _profile_controller_request("POST", "/workers/voice-change/stop")
_profile_controller_request("POST", "/workers/applio/stop") _profile_controller_request("POST", "/workers/applio/stop")
_profile_controller_request("POST", "/workers/trellis/stop") _profile_controller_request("POST", "/workers/trellis/stop")
_profile_controller_request("POST", "/workers/video/stop")
_restore_qwen(profile) _restore_qwen(profile)
STATE.mode = "llm" STATE.mode = "llm"
STATE.mode_phase = "ready" STATE.mode_phase = "ready"
@@ -601,7 +611,7 @@ def schedule_operating_mode(mode: str) -> tuple[bool, str]:
if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL: if not ENABLE_MUSIC_MODE or not PROFILE_CONTROL_URL:
raise RuntimeError("Musikmodus ist nicht konfiguriert") raise RuntimeError("Musikmodus ist nicht konfiguriert")
if mode not in {"llm", "music", "yue2", "separation", "voice", if mode not in {"llm", "music", "yue2", "separation", "voice",
"voicechange", "applio", "trellis"}: "voicechange", "applio", "trellis", "video"}:
raise ValueError("unbekannter Betriebsmodus") raise ValueError("unbekannter Betriebsmodus")
with STATE.lock: with STATE.lock:
if STATE.mode_phase not in {"ready", "error"}: if STATE.mode_phase not in {"ready", "error"}:
@@ -643,7 +653,8 @@ def _control_command(data: dict, path: str) -> str | None:
"/athena voicechange", "/athena changer", "/athena voicechange", "/athena changer",
"/athena applio", "/athena applio",
"/athena 3d", "/athena trellis", "/athena 3d", "/athena trellis",
"/athena status"} else None "/athena video", "/athena ltx",
"/athena ltx2", "/athena status"} else None
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -2129,6 +2140,8 @@ class Handler(BaseHTTPRequestHandler):
"applio_health": _worker_field("applio_health"), "applio_health": _worker_field("applio_health"),
"trellis_worker": _worker_field("trellis_worker"), "trellis_worker": _worker_field("trellis_worker"),
"trellis_health": _worker_field("trellis_health"), "trellis_health": _worker_field("trellis_health"),
"video_worker": _worker_field("video_worker"),
"video_health": _worker_field("video_health"),
"return_profile": state.get("return_profile"), "return_profile": state.get("return_profile"),
"last_error": STATE.mode_error, "last_error": STATE.mode_error,
"enabled": ENABLE_MUSIC_MODE, "enabled": ENABLE_MUSIC_MODE,
@@ -2139,7 +2152,7 @@ class Handler(BaseHTTPRequestHandler):
data = json.loads(self._read_body() or b"{}") data = json.loads(self._read_body() or b"{}")
mode = data.get("mode") if isinstance(data, dict) else None mode = data.get("mode") if isinstance(data, dict) else None
if mode not in {"llm", "music", "yue2", "separation", "voice", if mode not in {"llm", "music", "yue2", "separation", "voice",
"voicechange", "applio", "trellis"}: "voicechange", "applio", "trellis", "video"}:
raise ValueError("Feld 'mode' enthält einen unbekannten Betriebsmodus") raise ValueError("Feld 'mode' enthält einen unbekannten Betriebsmodus")
started, phase = schedule_operating_mode(mode) started, phase = schedule_operating_mode(mode)
self._send_json(202 if started else 200, { self._send_json(202 if started else 200, {
@@ -2807,7 +2820,8 @@ class Handler(BaseHTTPRequestHandler):
f"{mode['separator_worker']}. Voice Studio: " f"{mode['separator_worker']}. Voice Studio: "
f"{mode['voice_worker']}. Voice Changer: " f"{mode['voice_worker']}. Voice Changer: "
f"{mode['voice_change_worker']}. 3D Studio: " f"{mode['voice_change_worker']}. 3D Studio: "
f"{mode['trellis_worker']}. LLM-Profil: {profile or 'entladen'}.") f"{mode['trellis_worker']}. LTX-2 Studio: "
f"{mode['video_worker']}. LLM-Profil: {profile or 'entladen'}.")
else: else:
target = ("music" if command == "/athena music" else target = ("music" if command == "/athena music" else
"yue2" if command == "/athena yue2" else "yue2" if command == "/athena yue2" else
@@ -2816,6 +2830,7 @@ class Handler(BaseHTTPRequestHandler):
else "voicechange" if command in {"/athena voicechange", "/athena changer"} else "voicechange" if command in {"/athena voicechange", "/athena changer"}
else "applio" if command == "/athena applio" else "applio" if command == "/athena applio"
else "trellis" if command in {"/athena 3d", "/athena trellis"} else "trellis" if command in {"/athena 3d", "/athena trellis"}
else "video" if command in {"/athena video", "/athena ltx", "/athena ltx2"}
else "llm") else "llm")
try: try:
started, phase = schedule_operating_mode(target) started, phase = schedule_operating_mode(target)
@@ -3128,7 +3143,7 @@ def _startup_reconcile() -> None:
special_mode = previous.get("mode") special_mode = previous.get("mode")
if ENABLE_MUSIC_MODE and special_mode in {"music", "yue2", "separation", if ENABLE_MUSIC_MODE and special_mode in {"music", "yue2", "separation",
"voice", "voicechange", "applio", "voice", "voicechange", "applio",
"trellis"}: "trellis", "video"}:
STATE.mode = special_mode STATE.mode = special_mode
STATE.mode_phase = f"starting-{special_mode}" STATE.mode_phase = f"starting-{special_mode}"
_set_qwen_unavailable(True) _set_qwen_unavailable(True)