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
@@ -42,6 +42,8 @@ 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()
VIDEO_LABEL_KEY = "com.mike-ai.video-worker"
VIDEO_WORKER = os.environ.get("VIDEO_WORKER", "").strip()
LOCK = threading.Lock()
log = logging.getLogger("profile-controller")
@@ -187,6 +189,22 @@ def trellis_container() -> dict:
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:
if MUSIC_WORKER:
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_voice_tools()
stop_trellis_if_configured()
stop_video_if_configured()
for other in image_containers():
if other["Id"] != item["Id"]:
stop_container(other, timeout=20)
@@ -319,6 +338,7 @@ def set_music_worker(running: bool) -> dict:
stop_voice_tools()
stop_yue2_if_configured()
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -340,6 +360,7 @@ def set_yue2_worker(running: bool) -> dict:
stop_separator_if_configured()
stop_voice_tools()
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -361,6 +382,7 @@ def set_separator_worker(running: bool) -> dict:
stop_yue2_if_configured()
stop_voice_tools()
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -383,6 +405,7 @@ def set_voice_worker(running: bool) -> dict:
stop_separator_if_configured()
stop_voice_tools("voice")
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -405,6 +428,7 @@ def set_voice_change_worker(running: bool) -> dict:
stop_separator_if_configured()
stop_voice_tools("voicechange")
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -427,6 +451,7 @@ def set_applio_worker(running: bool) -> dict:
stop_separator_if_configured()
stop_voice_tools("applio")
stop_trellis_if_configured()
stop_video_if_configured()
start_container(item)
else:
stop_container(item, timeout=30)
@@ -455,6 +480,28 @@ def set_trellis_worker(running: bool) -> dict:
"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:
items = items or containers()
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_voice_tools()
stop_trellis_if_configured()
stop_video_if_configured()
start_container(tts_container())
items = containers()
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
"starting" if trellis.get("State") == "running" else
"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),
"music_worker": music.get("State", "disabled"),
"music_health": music_health,
@@ -603,6 +658,8 @@ class Handler(BaseHTTPRequestHandler):
"applio_health": applio_health,
"trellis_worker": trellis.get("State", "disabled"),
"trellis_health": trellis_health,
"video_worker": video.get("State", "disabled"),
"video_health": video_health,
"profiles": {name: items.get(name, {}).get(
"State", "missing") for name in ALLOWED}})
except Exception as exc:
@@ -669,6 +726,13 @@ class Handler(BaseHTTPRequestHandler):
log.exception("TRELLIS worker transition failed")
self.reply(503, {"error": str(exc)})
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 = {
"/workers/image/start": (IMAGE_WORKER, True),
"/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 8013 trellis-studio:8080
start_proxy 8014 yue2-studio:8014
start_proxy 8015 ltx2-studio:8015
start_proxy 8202 mcp-athena-operator:8000
start_proxy 9443 portainer:9443
@@ -18,7 +18,7 @@ correct the durable source when the user requested maintenance.
## 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
request: it temporarily pauses the active text profile and Qwen3-TTS, runs the
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
container is expected. A candidate is stale only after checking Compose,
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 ACESTEP_GPU_UUID=${ACESTEP_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() {
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/stack/experiments/applio-rvc
create_project /opt/mike-ai/Mikes-Applio-UI compose.example.yaml
create_project /opt/mike-ai/ltx2-studio
printf 'ATHENA_SPECIALISTS_REBUILT_OK\n'