"""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()