106 lines
7.0 KiB
Python
106 lines
7.0 KiB
Python
"""Pinned, unprivileged LTX Desktop backend installation; no weights installed."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import threading
|
|
import time
|
|
import uuid
|
|
|
|
DESKTOP_REV='68cd86c15e5fd25f56229ea63c0dbcb0338f7812'
|
|
LTX_REV='d151147788a9284cca791edc6ce898007e727fe6'
|
|
|
|
class LTXOriginalRuntime:
|
|
def __init__(self,root,image_runtime):
|
|
self.root=Path(root);self.image_runtime=image_runtime;self.lock=threading.RLock();self.cancel=threading.Event();self.process=None;self.worker=None;self.job=None
|
|
path=self.root/'installation.json'
|
|
if path.is_file():
|
|
self.job=json.loads(path.read_text())
|
|
if self.job.get('state')=='running':self.job.update(state='interrupted',phase='Installation unterbrochen. Erneuter Versuch möglich.')
|
|
def paths(self):
|
|
path=self.root/'active.json'
|
|
if path.is_file():
|
|
ident=json.loads(path.read_text()).get('id','')
|
|
if len(ident)==32 and all(c in '0123456789abcdef' for c in ident):
|
|
base=self.root/ident
|
|
if (base/'ready').is_file():return base/'python/bin/python',base/'desktop/backend'
|
|
return self.root/'missing-python',self.root/'missing-backend'
|
|
def status(self):
|
|
python,backend=self.paths();shared=self.image_runtime.paths()[0].is_file()
|
|
return dict(installed=python.is_file() and (backend/'ltx2_server.py').is_file(),desktop_revision=DESKTOP_REV,ltx_revision=LTX_REV,job=dict(self.job) if self.job else None,required_disk_gib=5 if shared else 25,shared_cuda=shared,origin='Lightricks/LTX-Desktop + Lightricks/LTX-2',api='Originale LTX-Desktop-API',gpu_policy='5080: Transformer/VAE · 3060: Textencoder · Gewichtsstreaming von Disk/CPU; keine vollständige BF16-Residenz')
|
|
def _save(self):
|
|
tmp=self.root/'installation.tmp';tmp.write_text(json.dumps(self.job));tmp.replace(self.root/'installation.json')
|
|
def _phase(self,phase):
|
|
with self.lock:self.job['phase']=phase;self._save()
|
|
def start(self):
|
|
with self.lock:
|
|
if self.job and self.job['state']=='running':raise ValueError('LTX-Installation läuft bereits.')
|
|
if self.status()['installed']:raise ValueError('LTX Original ist bereits installiert.')
|
|
if sys.platform!='linux' or not shutil.which('git'):raise ValueError('Debian/Linux mit Git und Python venv erforderlich.')
|
|
self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
|
need=self.status()['required_disk_gib']
|
|
if shutil.disk_usage(self.root).free<need*1024**3:raise ValueError(f'Mindestens {need} GiB freier Speicher erforderlich.')
|
|
self.cancel.clear();self.job=dict(id=uuid.uuid4().hex,state='running',phase='Vorbereitung',started_at=time.time());self._save()
|
|
self.worker=threading.Thread(target=self._run,daemon=True);self.worker.start();return self.status()
|
|
def stop(self):
|
|
self.cancel.set()
|
|
process=self.process
|
|
if process and process.poll() is None:
|
|
try:os.killpg(process.pid,signal.SIGTERM)
|
|
except ProcessLookupError:pass
|
|
if self.worker:self.worker.join(7)
|
|
return {'cancellation_requested':True}
|
|
def log(self):
|
|
path=self.root/'installation.log'
|
|
return {'text':path.read_text(errors='replace')[-18000:] if path.exists() else ''}
|
|
def _command(self,args,env=None):
|
|
if self.cancel.is_set():raise InterruptedError()
|
|
with (self.root/'installation.log').open('a') as output:
|
|
self.process=subprocess.Popen([str(a) for a in args],stdout=output,stderr=subprocess.STDOUT,start_new_session=True,env=env)
|
|
process=self.process;deadline=time.monotonic()+3600
|
|
while process.poll() is None:
|
|
if self.cancel.is_set() or time.monotonic()>deadline:
|
|
try:os.killpg(process.pid,signal.SIGTERM);process.wait(5)
|
|
except subprocess.TimeoutExpired:os.killpg(process.pid,signal.SIGKILL);process.wait()
|
|
except ProcessLookupError:pass
|
|
if self.cancel.is_set():raise InterruptedError()
|
|
raise RuntimeError('Zeitlimit beim Installationsschritt erreicht.')
|
|
time.sleep(.2)
|
|
if self.cancel.is_set():raise InterruptedError()
|
|
if process.returncode:raise RuntimeError('Installationsschritt fehlgeschlagen. Eigene Installationsausgabe prüfen.')
|
|
def _run(self):
|
|
base=self.root/self.job['id'];state='failed';phase='Installation fehlgeschlagen.'
|
|
try:
|
|
base.mkdir(mode=0o700);tmp=base/'tmp';tmp.mkdir()
|
|
env=dict(os.environ,TMPDIR=str(tmp),PIP_NO_INPUT='1',PIP_DISABLE_PIP_VERSION_CHECK='1',GIT_TERMINAL_PROMPT='0',OMP_NUM_THREADS='2')
|
|
# Expose immutable shared packages through a .pth in the NEW environment, never modify ComfyUI.
|
|
shared=self.image_runtime.paths()[0];bootstrap=shared if shared.is_file() else Path(sys.executable)
|
|
self._phase('Isolierte Python-Umgebung erstellen');self._command([bootstrap,'-m','venv',base/'python'],env)
|
|
python=base/'python/bin/python'
|
|
if shared.is_file():
|
|
self._command([python,'-c','import json,site,subprocess,sys; p=json.loads(subprocess.check_output([sys.argv[1],"-c","import json,site;print(json.dumps(site.getsitepackages()))"],text=True));from pathlib import Path;Path(site.getsitepackages()[0],"deck-shared-cuda.pth").write_text("\\n".join(p)+"\\n")',shared],env)
|
|
self._command([python,'-c','import sys;assert sys.version_info >= (3,12), "LTX Desktop benötigt Python >=3.12"'],env)
|
|
for name,url,revision in [('desktop','https://github.com/Lightricks/LTX-Desktop.git',DESKTOP_REV),('ltx','https://github.com/Lightricks/LTX-2.git',LTX_REV)]:
|
|
self._phase('Originalquelle herunterladen: '+name)
|
|
path=base/name;self._command(['git','init',path],env);self._command(['git','-C',path,'fetch','--depth','1',url,revision],env);self._command(['git','-C',path,'checkout','--detach','FETCH_HEAD'],env)
|
|
self._phase('LTX-Abhängigkeiten installieren · CUDA-Pakete wiederverwenden')
|
|
# Fixed inference versions; sources are independently pinned above. No model downloads.
|
|
self._command([python,'-m','pip','install','--no-cache-dir','torch==2.11.0','torchvision==0.26.0','torchaudio==2.11.0','--index-url','https://download.pytorch.org/whl/cu128'],env)
|
|
self._command([python,'-m','pip','install','--no-cache-dir','-r',Path(__file__).parent/'deploy/ltx-original-requirements.lock'],env)
|
|
for package in ('ltx-core','ltx-pipelines'):self._command([python,'-m','pip','install','--no-cache-dir','--no-deps',base/'ltx/packages'/package],env)
|
|
self._phase('Import, CUDA und API-Abhängigkeiten prüfen')
|
|
self._command([python,'-m','pip','check'],env)
|
|
self._command([python,'-c','import torch,torchaudio,fastapi,transformers;from ltx_pipelines.utils.blocks import PromptEncoder;assert torch.version.cuda;assert hasattr(PromptEncoder,"__call__")'],env)
|
|
if self.cancel.is_set():raise InterruptedError()
|
|
(base/'ready').write_text(DESKTOP_REV+'\n'+LTX_REV)
|
|
marker=self.root/'active.tmp';marker.write_text(json.dumps({'id':base.name}));marker.replace(self.root/'active.json')
|
|
state='complete';phase='LTX Original installiert. Gewichte laden erst bei Generierung.'
|
|
except InterruptedError:state='cancelled';phase='Installation abgebrochen.'
|
|
except Exception as exc:phase=str(exc)
|
|
finally:
|
|
if state!='complete':shutil.rmtree(base,ignore_errors=True)
|
|
with self.lock:self.process=None;self.job.update(state=state,phase=phase,finished_at=time.time());self._save()
|