Add selectable native LTX Original runtime and DeskWEB API integration
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""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.')
|
||||
Reference in New Issue
Block a user