Files
Athena-Deck/video_original.py
T

76 lines
5.5 KiB
Python

"""Selectable native LTX Original and ComfyUI, sharing Deck's GPU admission gate."""
import json
import os
from pathlib import Path, PurePosixPath
import shutil
import socket
import subprocess
import time
import urllib.request
from video_comfy import VideoComfy
class VideoRuntimes(VideoComfy):
def __init__(self,*args,original_runtime):
self.original_runtime=original_runtime
super().__init__(*args)
def runtime_kind(self):return self.selection.get('runtime','comfy')
def models(self):
rows=super().models()
if self.runtime_kind()=='original':
installed=self.original_runtime.status()['installed']
for row in rows:
row['blockers']=[x for x in row['blockers'] if not x.startswith('Passende ComfyUI')]
if not installed:row['blockers'].append('LTX Original fehlt. Unter Einstellungen → Laufzeiten → LTX Original installieren.')
row['runnable']=not row['blockers'];row['state']='ready' if row['runnable'] else 'configured';row['label']='LTX Original bereit · lädt bei Anfrage' if row['runnable'] else 'Voraussetzungen fehlen'
return rows
def select_runtime(self,kind):
if kind not in ('comfy','original'):raise ValueError('Laufzeit muss comfy oder original sein.')
with self.lock:
if self.scheduler.gpu_mode!='llm' or self.state=='switching':raise ValueError('Zuerst auf LLM wechseln; laufende Video-API wird nicht umgebogen.')
self.selection['runtime']=kind;self._save_selection()
return self.status()
def _save_selection(self):
self.root.mkdir(parents=True,exist_ok=True);temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(self.selection));temp.replace(self.path)
def select(self,ident):
kind=self.runtime_kind();super().select(ident)
self.selection['runtime']=kind;self._save_selection();return self.status()
def status(self):
result=super().status();result['runtime_kind']=self.runtime_kind();result['runtimes']={'comfy':self.runtime.status(),'original':self.original_runtime.status()}
if self.runtime_kind()=='original':
if result.get('error') and result['error'].startswith('ComfyUI wurde'):result['error']='LTX Original wurde unerwartet beendet. Auf LLM wechseln und Backend erneut starten.'
result['runtime']=self.original_runtime.status();result['memory_policy']='LTX Original: API startet im Videomodus; Gewichte laden bei Generierung. LLM-Wechsel beendet den gesamten Backend-Prozess.'
if result['service']:
result['service'].update(api_url='/api/… (Originale LTX-Desktop-API auf dem gemeinsamen API-Port)',message='5080: Transformer/VAE · 3060: Textencoder · BF16-Gewichtsstreaming; Bedienung in LTX DeskWEB.',ui_url='http://127.0.0.1:8118')
return result
def _start(self):
if self.runtime_kind()=='comfy':return super()._start()
raw=subprocess.check_output(['nvidia-smi','--query-gpu=uuid,name,memory.used','--format=csv,noheader,nounits'],text=True)
gpus=[line.split(',') for line in raw.strip().splitlines()];ordered=[next((g for g in gpus if name in g[1]),None) for name in ('5080','3060')]
if any(g is None for g in ordered):raise ValueError('RTX 5080 und RTX 3060 benötigt.')
if any(float(g[2])>256 for g in ordered):raise ValueError('GPUs nicht frei; fremde Prozesse werden nicht beendet.')
row=next(x for x in self.models() if x['model']['id']==self.selection['model_id'])
work=self.root/'original-work';models=work/'models/ltx-2.5';models.mkdir(parents=True,exist_ok=True)
for ident in [row['model']['id'],*row['components'].values()]:
entry=self.catalog.entry(ident);source=self.catalog.root/ident/('model'+PurePosixPath(entry['file']).suffix);link=models/PurePosixPath(entry['file']).name
if link.exists() and not link.is_symlink():raise ValueError('LTX-Modellansicht enthält eine fremde Datei.')
if link.is_symlink():link.unlink()
link.symlink_to(source.resolve())
settings_path=work/'settings.json';settings=json.loads(settings_path.read_text()) if settings_path.is_file() else {}
settings.update(modelsDir=str(work/'models'),activeLtxModelId='ltx-2.5-22b-distilled',useLocalTextEncoder=True,useConvVae=False)
if not settings_path.is_file():settings.update(promptEnhancerEnabledT2V=False,promptEnhancerEnabledI2V=False)
settings_path.write_text(json.dumps(settings))
for name in ('outputs','remote-inputs/deskweb'):(work/name).mkdir(parents=True,exist_ok=True)
with socket.socket() as sock:sock.bind(('127.0.0.1',0));self.port=sock.getsockname()[1]
for name in ('temp','cuda-cache','triton-cache','torch-cache'):(work/name).mkdir(exist_ok=True)
python,backend=self.original_runtime.paths();env=dict(os.environ,CUDA_VISIBLE_DEVICES=','.join(g[0].strip() for g in ordered),OMP_NUM_THREADS='2',HF_HUB_DISABLE_TELEMETRY='1',TMPDIR=str(work/'temp'),CUDA_CACHE_PATH=str(work/'cuda-cache'),TRITON_CACHE_DIR=str(work/'triton-cache'),TORCHINDUCTOR_CACHE_DIR=str(work/'torch-cache'))
self.switch_phase='Originale LTX-API starten · Gewichte laden erst bei Anfrage'
self.process=subprocess.Popen([str(python),str(Path(__file__).with_name('ltx_original_entry.py')),'--backend',str(backend),'--data',str(work),'--port',str(self.port)],env=env,cwd=backend,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,start_new_session=True)
deadline=time.monotonic()+180
while time.monotonic()<deadline:
if self.process.poll() is not None:raise ValueError('LTX-Backend-Start fehlgeschlagen; Installation und Komponenten prüfen.')
try:
with urllib.request.urlopen(f'http://127.0.0.1:{self.port}/health',timeout=2) as response:
if response.status==200:return
except (OSError,TimeoutError):time.sleep(.5)
raise ValueError('LTX-API ist noch nicht bereit.')