75 lines
5.2 KiB
Python
75 lines
5.2 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]
|
|
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')
|
|
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.')
|