Add exclusive LTX-2 video studio profile
This commit is contained in:
@@ -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
@@ -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
@@ -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"]
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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'
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user