diff --git a/BACKUP.md b/BACKUP.md index cc52ebd..bd3e740 100644 --- a/BACKUP.md +++ b/BACKUP.md @@ -6,7 +6,7 @@ - Alle Deck-Modellprofile mit Sampling, Kontext, Slots, GPU-Verteilung, KV-Cache, MTP, Vision-Projektor, Komponenten und Prompt-Aufwertern. - API-Port, freigegebene Profile, Listener-Autostart, aktive Video-Modellauswahl und TTS-/STT-Residency-Einstellungen. -- Modellinventar mit festen Hugging-Face-Commits, Dateinamen, Größe und SHA-256. Laufzeitrezepte und vorhandene llama.cpp-Buildversionen, inklusive aktivem Build. +- Modellinventar mit festen Hugging-Face-Commits, Dateinamen, Größe und SHA-256. Laufzeitrezepte und vorhandene llama.cpp-Buildversionen, inklusive aktivem Build. Installierte LTX-Original-Laufzeit mit gepinnten Quellversionen, Video-Laufzeitauswahl und originale LTX-Backend-Einstellungen. - Oberflächenkennwort als Prüfwert, API-Token als Prüfwert, Hugging-Face-Token und Deck-eigener ComfyUI-Diensttoken. API-Token im Klartext kann Deck nicht rekonstruieren: der bereits im Client konfigurierte Token bleibt nach Wiederherstellung gültig. - Deck-eigene WireGuard-Konfiguration einschließlich Schlüsseln, gewünschter Verbindung und Zugriffsmodus, wenn das Deck-Netzwerkmodul angebunden ist. - Ausdrücklich mit `io.athena-deck.managed=true` und `io.athena-deck.role=application` markierte Container: Image-/Registryquelle, Labels, Startparameter, Umgebungsvariablen, Benutzer, Ressourcenlimits, Restart-Regel, Ports, Mount-Zuordnung und Startzustand. Eingebundene Secret-Dateien und Swarm `Settings.fds`/`Backends.fds` werden mitgesichert. Swarm erhält das im Deck-Quellstand gepflegte Build-Rezept. diff --git a/LTX_ORIGINAL.md b/LTX_ORIGINAL.md new file mode 100644 index 0000000..d170a28 --- /dev/null +++ b/LTX_ORIGINAL.md @@ -0,0 +1,102 @@ +# LTX Original + +LTX Original ist eine zusätzliche native Python-Laufzeit für die originale +LTX-Desktop-API. LTX DeskWEB bleibt eine separate Docker-Oberfläche. ComfyUI +bleibt verfügbar, wird nicht ersetzt und übernimmt weiterhin Bildgenerierung. + +## Installation und Verwendung + +1. **Einstellungen → Laufzeiten → LTX Original** öffnen und installieren. + Installation und Abbruch sind echte serverseitige Aktionen. Die eigene + Installationsausgabe, Phase und festgelegten Versionen werden angezeigt. +2. Im Bereich **Video → Laufzeit / Aktives Modell** `LTX Original` wählen. + Die Laufzeitauswahl ist nur im LLM-Modus möglich. Das heruntergeladene + LTX-2.5-Distilled-Modell aktivieren; fehlende Komponenten dort nachladen. +3. In der Übersicht auf **Video** wechseln. Deck beendet nur eigene GPU-Worker, + prüft freie GPUs, startet das Backend und wartet auf dessen Health-Endpunkt. + Gewichte laden bei Generierung, nicht beim API-Start. +4. LTX DeskWEB öffnen. Es leitet die originale API über den gemeinsamen + Deck-API-Port weiter. Auflösung, Dauer, Bilder und Audio gehören in DeskWEB. +5. Auf **LLM** wechseln: der komplette Backend-Prozess endet, einschließlich + seiner GPU-Allokationen. Ein Deck-Neustart beendet ebenfalls seinen Worker. + +Laufzeitinstallation benötigt Debian/Linux, Git, Python >=3.12 und venv. +Die bestehende CUDA-/PyTorch-Umgebung der Bildlaufzeit wird über einen lesenden +Python-Suchpfad wiederverwendet; LTX-spezifische Pakete liegen in einem neuen +Overlay. ComfyUI-Pakete werden nicht geändert. Ohne vorhandene Umgebung werden +CUDA-Pakete separat installiert. Freier Platz: mindestens 5 bzw. 25 GiB. +Die Installation lädt keine Modellgewichte und startet keine Inferenz. + +Quellen und unveränderliche Revisionen: + +- [LTX Desktop](https://github.com/Lightricks/LTX-Desktop), + `68cd86c15e5fd25f56229ea63c0dbcb0338f7812` (passend zum DeskWEB-Import). +- [LTX-2](https://github.com/Lightricks/LTX-2), + `d151147788a9284cca791edc6ce898007e727fe6` / v1.2.0. +- Originale Lizenz/Notices bleiben im heruntergeladenen Checkout. Die + Modell-Lizenz ist davon unabhängig. Keine automatische Umstellung auf HEAD. + +## Prozess und Speicher + +`state/ltx-original-runtime/` enthält Quellcode und Python-Umgebung; +`state/video/original-work` enthält Backend-Einstellungen, Ein-/Ausgaben und +Symlinks zur Deck-Bibliothek. Gewichte werden nicht kopiert. + +Die logischen Geräte werden über GPU-UUIDs geordnet: CUDA0 ist RTX 5080, +CUDA1 ist RTX 3060. Transformer und VAE laufen auf CUDA0. Eine kleine +Deployment-Erweiterung des originalen PromptEncoder führt die Textkodierung auf +CUDA1 aus und überträgt ausschließlich das Conditioning zurück auf CUDA0. +API-Schemas und Generierungspipelines werden nicht in eine generische API übersetzt. + +BF16-Transformer und BF16-Textencoder passen nicht vollständig in den VRAM. +Das originale `OffloadMode.DISK` streamt Gewichte über begrenzte CPU-Puffer. +Das ist absichtlich keine Zusage vollständiger GPU-Residenz oder der besten +Geschwindigkeit. Es vermeidet das vollständige Pinnen der riesigen BF16-Gewichte +im auf 32 GiB begrenzten Deck-Testcontainer. Die Auswirkungen auf Geschwindigkeit +müssen anhand echter Generierungen geprüft werden. + +Der Backend-Port ist dynamisch und nur auf Loopback erreichbar. Der gemeinsame +Deck-Port leitet im Video-Modus die originale HTTP-API weiter und schützt sie +mit dem vorhandenen API-Token. ComfyUI verwendet weiterhin seine eigene API. +Die Web-Oberfläche selbst erhält keinen Docker-Socket oder GPU-Zugriff. + +## DeskWEB anbinden + +Backend-URL bleibt `http://127.0.0.1:8120` bei der aktuellen Athena-Installation. +Der Token bleibt serverseitig. Wichtig: UI und Backend müssen dieselben Medien +sehen. Für die native Laufzeit sind die neuen Mounts: + +- Host `.../state/video/original-work/remote-inputs/deskweb` → Web `/data/inputs`. +- Host `.../state/video/original-work/outputs` → Web `/data/outputs:ro`. +- `BACKEND_INPUT_DIR=/var/lib/deck/video/original-work/remote-inputs/deskweb` +- `BACKEND_OUTPUT_DIR=/var/lib/deck/video/original-work/outputs` + +Die Backend-Pfade beziehen sich auf die derzeitige Deck-Testcontaineransicht. +Beim nativen systemd-Produkt müssen die tatsächlichen absoluten Hostpfade gesetzt +werden. Schreibende Eingaben benötigen eine gemeinsame Gruppe; nur dieses neue +Medienverzeichnis provisionieren, keine rekursive Änderung alter LTX-Daten. +Vorhandene Projekte mit alten Medienpfaden migrieren nicht automatisch. + +## Grenzen und API + +Die fünf vorhandenen LTX-2.5-Dateien decken das Basisrezept ab. Zusätzliche +LoRAs, Control-Modelle, AutoDuration und lokale Prompt-Aufwertung können weitere +Gewichte benötigen. Die originale API meldet diese Voraussetzungen; das +Basisrezept behauptet nicht, alle optionalen Desktop-Funktionen freizuschalten. +Bisherige lokale Desktop-Erweiterungen sind nicht automatisch im Original-Backend. + +- GET `/api/v1/ltx-original-runtime` / `/log` +- POST `/api/v1/ltx-original-runtime/install` / `/cancel` mit `{}` +- POST `/api/v1/video/runtime` mit `{"runtime":"original"}` oder `comfy` +- Bestehende `/api/v1/video/service` und `/mode` bleiben erhalten. + +Alle Verwaltungsaktionen benötigen die Deck-Administratorsitzung und den +bestehenden POST-/Origin-Schutz. Backup exportiert Auswahl, Einstellungen und +festgelegtes Runtime-Rezept; Gewichte und Python-Pakete werden nachgeladen. + +## Integrationsstand am 30.09.2026 + +Lokale Verwaltungs-, Auswahl-, Backup-, Gerätepolicy- und Transporttests sind +geprüft. Die erste Installation auf Athena, DeskWEB-Mountumstellung und echte +CUDA-Generierung stehen noch aus: die LAN-/SSH-Verbindung fiel während der +Integration aus. Die vorhandenen Remote-Dienste wurden bisher nicht geändert. diff --git a/README.md b/README.md index af3a16c..9b777f1 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,14 @@ Bildvarianten kann Deck eine vorhandene ComfyUI-Installation als mögliche Basis anzeigen. Ohne geprüftes Komponentenrezept und Workflow bleibt das Modell gesperrt; ein weiterer Runtime-Build wird daraus nicht automatisch abgeleitet. +## LTX Original und LTX DeskWEB + +Unter **Einstellungen → Laufzeiten → LTX Original** wird das gepinnte originale +LTX-Desktop-Backend separat installiert. Vorhandene Modellgewichte werden +per Dateiansicht wiederverwendet. Im Bereich Video kann zwischen ComfyUI und +LTX Original gewählt werden; LTX DeskWEB benötigt LTX Original. Details: +[LTX Original](LTX_ORIGINAL.md). + ## Zugang und API-Token Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet. diff --git a/app.js b/app.js index abaaa27..b52079d 100644 --- a/app.js +++ b/app.js @@ -24,6 +24,7 @@ function appearancePage(){ sync(); } async function refresh(){if(refreshing)return;refreshing=true;const page=location.hash.slice(1)||'dashboard';document.querySelectorAll('nav a').forEach(a=>a.classList.toggle('active',a.hash==='#'+page));try{error.textContent='';let html; +if(page==='ltx-original-runtime'){LTXOriginalUI.render();return;} if(['video','video-runtime'].includes(page)){VideoUI.runtime();return;} if(['chat','image','audio','stt','music','voice','video','runtime','runtimes','image-runtime','stt-runtime','tts-runtime','docker-runtime','services'].includes(page)){Studio.render(page);return;} if(page==='backup'){BackupUI.render();return;} diff --git a/backup.py b/backup.py index 1a8674c..a3a1802 100644 --- a/backup.py +++ b/backup.py @@ -7,7 +7,7 @@ from auth import atomic_write,validate_record from backup_codec import seal,open_backup from catalog import repo_id,file_role,KINDS from profiles import SCHEMAS,CHAT_GPU_DEFAULTS,chat_parameters -SETTINGS=('profiles.json','endpoint.json','audio-policy.json','video/comfy-selection.json','models/huggingface.json','video/comfy-client-token') +SETTINGS=('profiles.json','endpoint.json','audio-policy.json','video/comfy-selection.json','models/huggingface.json','video/comfy-client-token','video/original-work/settings.json') MAX_HISTORY=40*1024**2 VERSION=1 # Exact byte hash verified against official Hub metadata, not a filename heuristic. @@ -63,14 +63,17 @@ def validate(doc): policy=settings.get('audio-policy.json',{}) if not isinstance(policy,dict) or set(policy)-{'tts','stt'} or any(not isinstance(v,dict) or set(v)!={'mode','profile_id'} or v['mode'] not in ('auto','warm','per_request') or v['profile_id'] is not None and v['profile_id'] not in pids for v in policy.values()):raise ValueError('Ungültige Audio-Einstellung.') selection=settings.get('video/comfy-selection.json',{}) - if not isinstance(selection,dict) or set(selection)-{'model_id'} or selection.get('model_id') and selection['model_id'] not in ids:raise ValueError('Ungültige Video-Auswahl.') + if isinstance(selection,dict) and selection.get('runtime','comfy') not in ('comfy','original'):raise ValueError('Unbekannte Video-Laufzeit.') + if not isinstance(selection,dict) or set(selection)-{'model_id','runtime'} or selection.get('model_id') and selection['model_id'] not in ids:raise ValueError('Ungültige Video-Auswahl.') if 'models/huggingface.json' in settings: hf=settings['models/huggingface.json'] if not isinstance(hf,dict) or set(hf)!={'token'} or hf['token'] is not None and (not isinstance(hf['token'],str) or not re.fullmatch(r'hf_[A-Za-z0-9]{10,252}',hf['token'])):raise ValueError('Ungültiger Hugging-Face-Zugang.') if 'video/comfy-client-token' in settings and not re.fullmatch(r'[A-Za-z0-9_-]{32,256}',settings['video/comfy-client-token'].strip()):raise ValueError('Ungültiger Diensttoken.') + if 'video/original-work/settings.json' in settings and (not isinstance(settings['video/original-work/settings.json'],dict) or len(json.dumps(settings['video/original-work/settings.json']))>65536):raise ValueError('Ungültige LTX-Einstellungen.') validate_record(doc.get('credentials')) runtimes=doc.get('runtimes') - if not isinstance(runtimes,dict) or set(runtimes)!={'llama','llama_builds','image','tts','enhancers','swarm_nodes'}:raise ValueError('Ungültiges Laufzeitrezept.') + if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.') + if 'ltx_original' in runtimes and type(runtimes['ltx_original']) is not bool:raise ValueError('Ungültige LTX-Laufzeit.') if not isinstance(runtimes['llama_builds'],list) or len(runtimes['llama_builds'])>50 or any(not isinstance(b,dict) or set(b)!={'commit','backend'} or not re.fullmatch('[a-f0-9]{40}',b.get('commit','')) or b.get('backend') not in ('CUDA','CPU') for b in runtimes['llama_builds']):raise ValueError('Ungültige Buildrezepte.') if runtimes['llama'] is not None: item=runtimes['llama'] @@ -134,7 +137,10 @@ class Backup: if not s.network.disabled: network=s.network.call('backup-export') else:warnings.append('Deck-Netzwerkmodul deaktiviert; fremder WireGuard-Gateway wird nicht gesichert.') - document=dict(format='athena-deck',version=VERSION,created_at=time.time(),settings=settings,models=models,credentials=s.credentials.read(),theme=theme,network=network,services=services,runtime_versions=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']),runtimes=dict(llama=llama,llama_builds=[dict(commit=b['commit'],backend=b['backend']) for b in state['builds']],image=s.image_runtime.status()['installed'],tts=s.tts_runtime.status()['installed'],enhancers={task:data['revision'] for task in ('t2i','i2i') if (data:=s.prompt_enhancer.installed(task))},swarm_nodes=(self.root/'video/swarm-comfy-nodes').is_dir()),warnings=warnings) + document=dict(format='athena-deck',version=VERSION,created_at=time.time(),settings=settings,models=models,credentials=s.credentials.read(),theme=theme,network=network,services=services,runtime_versions=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']),runtimes=dict(ltx_original=bool(getattr(s,'ltx_original_runtime',None) and s.ltx_original_runtime.status()['installed']),llama=llama,llama_builds=[dict(commit=b['commit'],backend=b['backend']) for b in state['builds']],image=s.image_runtime.status()['installed'],tts=s.tts_runtime.status()['installed'],enhancers={task:data['revision'] for task in ('t2i','i2i') if (data:=s.prompt_enhancer.installed(task))},swarm_nodes=(self.root/'video/swarm-comfy-nodes').is_dir()),warnings=warnings) + if document['runtimes']['ltx_original']: + from ltx_original_runtime import DESKTOP_REV,LTX_REV + document['runtime_versions']['ltx_original']=dict(desktop=DESKTOP_REV,inference=LTX_REV) if include_history: with tempfile.TemporaryDirectory(dir=self.directory) as work: out=Path(work)/'history.sqlite3' @@ -161,7 +167,10 @@ class Backup: elif s.network.status().get('enabled') or s.network.status().get('mode')!='lan':blockers.append('Deck-Tunnel vor Netzwerk-Restore im bestätigten LAN-Modus deaktivieren.') if doc['network']['mode']!='lan':warnings.append('Tunnelzugang wird mit Rückfallzeit aktiviert und muss über den Tunnel bestätigt werden.') versions=doc.get('runtime_versions',{}) - if versions and versions!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') + if versions.get('ltx_original'): + from ltx_original_runtime import DESKTOP_REV,LTX_REV + if versions['ltx_original']!=dict(desktop=DESKTOP_REV,inference=LTX_REV):blockers.append('Backup verlangt eine andere originale LTX-Version. Passende Deck-Version verwenden.') + if versions and {k:v for k,v in versions.items() if k!='ltx_original'}!=dict(image=dict(comfy=s.image_runtime.status()['comfy_revision'],gguf=s.image_runtime.status()['gguf_revision']),tts=s.tts_runtime.status()['revision']):blockers.append('Backup verlangt andere Bild-/TTS-Laufzeitversionen. Passende Deck-Version oder geprüftes Runtime-Upgrade verwenden.') for c in doc['services']: if not c.get('registry') and c.get('build_recipe')!='swarm-ui':warnings.append(c['name']+': Image nur lokal vorhanden; Registry-/Buildquelle fehlt.') ports=s.endpoint.allowed_ports @@ -186,7 +195,7 @@ class Backup: if len(set(data['services']))!=len(data['services']) or any(name not in services for name in data['services']):raise ValueError('Unbekannter Zusatzdienst.') if any(not services[name].get('registry') and not services[name].get('build_recipe') for name in data['services']):raise ValueError('Gewählter Dienst hat keine Wiederherstellungsquelle.') s=self.server - if s.catalog.pending or s.catalog.job and s.catalog.job['state']=='downloading' or s.runtime.busy or any((getattr(o,'job',None) or {}).get('state')=='running' for o in (s.image_runtime,s.tts_runtime,s.prompt_enhancer,s.auto_tests,s.chat_tests,s.image_tests,s.tts_tests,s.stt)):raise ValueError('Zuerst laufende Downloads, Builds und Tests abschließen oder abbrechen.') + if s.catalog.pending or s.catalog.job and s.catalog.job['state']=='downloading' or s.runtime.busy or any((getattr(o,'job',None) or {}).get('state')=='running' for o in (s.image_runtime,s.tts_runtime,s.ltx_original_runtime,s.prompt_enhancer,s.auto_tests,s.chat_tests,s.image_tests,s.tts_tests,s.stt)):raise ValueError('Zuerst laufende Downloads, Builds und Tests abschließen oder abbrechen.') self.cancel.clear();self.job=dict(id=secrets.token_hex(16),state='running',phase='Wiederherstellung vorbereiten',completed=0,total=len(doc['models'])+len(data['services'])+5,items=[],started_at=time.time(),theme=doc.get('theme','deck')) self._save();threading.Thread(target=self._restore,args=(copy.deepcopy(doc),data['services'],data['restore_credentials']),daemon=True).start();self.plan=None return self.status() @@ -212,6 +221,8 @@ class Backup: self._phase('Vorhandene Bild-/Videolaufzeit prüfen oder installieren');s.image_runtime.start();self._wait(s.image_runtime.status) if r['tts'] and not s.tts_runtime.status()['installed']: self._phase('TTS-Laufzeit und Zusatzdateien installieren');s.tts_runtime.start();self._wait(s.tts_runtime.status) + if r.get('ltx_original') and not s.ltx_original_runtime.status()['installed']: + self._phase('Originale LTX-Laufzeit installieren');s.ltx_original_runtime.start();self._wait(s.ltx_original_runtime.status) targets=r['llama_builds']+([r['llama']] if r['llama'] and r['llama'] not in r['llama_builds'] else []) for target in targets: state=s.runtime.status();matching=next((b for b in state['builds'] if b['commit']==target['commit'] and b['backend']==target['backend']),None) @@ -349,5 +360,5 @@ class Backup: with self.lock: self.cancel.set() if self.busy(): - self.server.catalog.stop();self.server.runtime.stop();self.server.image_runtime.stop();self.server.tts_runtime.stop();self.server.prompt_enhancer.stop() + self.server.catalog.stop();self.server.runtime.stop();self.server.image_runtime.stop();self.server.tts_runtime.stop();self.server.ltx_original_runtime.stop();self.server.prompt_enhancer.stop() return {'cancellation_requested':True} diff --git a/deploy/Dockerfile b/deploy/Dockerfile index f68249c..5c50044 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -15,8 +15,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptog WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock -COPY backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock +COPY ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/Dockerfile.app-update b/deploy/Dockerfile.app-update index 340cf43..5b8154f 100644 --- a/deploy/Dockerfile.app-update +++ b/deploy/Dockerfile.app-update @@ -5,8 +5,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends python3-cryptog WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock -COPY backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ -COPY backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ +COPY deploy/ltx-original-requirements.lock /app/deploy/ltx-original-requirements.lock +COPY ltx_original_runtime.py ltx_original_entry.py video_original.py backup.py backup_codec.py audio_policy.py prompt_enhancer.py prompt_enhancer_worker.py api_compat.py stt.py execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py image_upload.py profiles.py capacity.py server.py runtime.py video.py video_proxy.py video_comfy.py video_comfy_node.py video_comfy_proxy.py catalog.py hub_auth.py auth.py collect_hardware.py dashboard_data.py dashboard_history.py /app/ +COPY ltx-original-ui.js backup-ui.js video-ui.js stt-ui.js tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js dashboard-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css dashboard.css themes.css login.html login.js access-ui.js network-ui.js /app/ COPY deploy/docker_backup.py /app/deploy/docker_backup.py COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/install.py b/deploy/install.py index 7767632..24641b1 100644 --- a/deploy/install.py +++ b/deploy/install.py @@ -14,7 +14,7 @@ import urllib.request ROOT = Path(__file__).resolve().parent.parent LABEL = 'de.casaderoll.athena-deck.standalone' -FILES = ['backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] +FILES = ['ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','deploy/swarm-ui/Dockerfile','deploy/swarm-ui/patch-source.py','deploy/swarm-ui/proxy.py','deploy/swarm-ui/entrypoint.py','deploy/Dockerfile.app-update','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','prompt_enhancer.py','prompt_enhancer_worker.py','image_upload.py','audio_policy.py','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/import_dashboard_history.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] def run(*args, check=True, interactive=False): diff --git a/deploy/ltx-original-requirements.lock b/deploy/ltx-original-requirements.lock new file mode 100644 index 0000000..1fcf716 --- /dev/null +++ b/deploy/ltx-original-requirements.lock @@ -0,0 +1,24 @@ +# LTX Desktop 68cd86c; LTX-2 v1.2.0. Torch trio supplied separately (CUDA 12.8). +pillow==12.3.0 +huggingface-hub==1.7.1 +pynvml==13.0.1 +psutil==7.2.2 +pydantic==2.12.5 +transformers==5.14.1 +diffusers==0.39.0 +ftfy==6.3.1 +imageio==2.37.2 +imageio-ffmpeg==0.6.0 +peft==0.18.1 +protobuf==6.33.5 +sentencepiece==0.2.1 +opencv-python-headless==4.13.0.92 +fastapi==0.141.1 +starlette==1.6.0 +uvicorn==0.41.0 +python-multipart==0.0.32 +av==16.1.0 +openimageio==3.1.11.0 +cloudpickle==3.1.2 +accelerate==1.15.0 +scipy==1.17.1 diff --git a/execution_setup.py b/execution_setup.py index cb06e21..59fcf86 100644 --- a/execution_setup.py +++ b/execution_setup.py @@ -32,7 +32,7 @@ def assess(model, installed, profile=None): result.update(message='Dieses TTS-Modell wird noch nicht ausgeführt. Deck unterstützt derzeit die festgelegte Qwen3-TTS-CustomVoice-Variante mit eingebauten Stimmen. MLX, Base mit Referenzstimme und andere TTS-Architekturen benötigen andere Anbindungen.',alternative=dict(route='#tts-runtime',label='Unterstützte TTS-Alternative ansehen (Modellwechsel)')) if video_recipe(model): result['family']='LTX 2.5' - result.update(runtime='ComfyUI · LTX 2.5 (gemeinsame Bildlaufzeit)',state='runtime_missing',label='Komponenten und Video-Laufzeit einrichten',route='#video',components=[x['label'] for x in video_recipe(model).values()],message='Vorhandene ComfyUI-Installation wiederverwenden. Im Bereich Video werden Gewichte und passende Komponenten derselben Quellversion geprüft; keine zweite Laufzeit installieren.') + result.update(runtime='LTX Original oder ComfyUI · LTX 2.5',state='runtime_missing',label='Komponenten und Video-Laufzeit einrichten',route='#video',components=[x['label'] for x in video_recipe(model).values()],message='Im Bereich Video LTX Original für LTX DeskWEB oder die vorhandene ComfyUI-Laufzeit wählen. Gewichte und passende Komponenten derselben Quellversion werden wiederverwendet.') if not result['runtime']:return result result['installed']=bool(installed.get(kind)) if kind!='chat' and result['state']!='workflow_missing': diff --git a/index.html b/index.html index 85727c3..eb981a9 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
+Athena Deck
ATHENA CONTROL SURFACEv0.7 · Router
diff --git a/ltx-original-ui.js b/ltx-original-ui.js new file mode 100644 index 0000000..42b426b --- /dev/null +++ b/ltx-original-ui.js @@ -0,0 +1,11 @@ +window.LTXOriginalUI=(()=>{ + const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + async function api(path='',data){const r=await fetch('/api/v1/ltx-original-runtime'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const s=await r.json();if(!r.ok)throw Error(s.error||'Anfrage fehlgeschlagen');return s;} + function render(){const view=document.querySelector('#view');if(view.querySelector('#ltx-original-page'))return; + view.innerHTML='

LTX Original

Originales Lightricks-Backend für die LTX-Desktop-API. LTX DeskWEB bleibt die separate Oberfläche unter Weitere Dienste.

Installation

Eigene Installationsausgabe

Verwendung

Im Bereich Video die Laufzeit LTX Original wählen und das heruntergeladene LTX-Modell aktivieren. Danach in der Übersicht auf Video wechseln. Die originale API liegt am gemeinsamen API-Port; LTX DeskWEB spricht sie direkt an. Gewichte laden erst bei Generierung. Der Wechsel auf LLM beendet das Backend und gibt seinen GPU-Speicher frei.

5080: Transformer und VAE. 3060: Textencoder. Große BF16-Gewichte werden gestreamt; sie liegen nicht vollständig im VRAM. Zusätzliche Funktionen können weitere Komponenten benötigen.

Videomodell und Laufzeit wählen →
'; + const root=view.firstChild; + async function poll(){if(!root.isConnected)return;try{const s=await api();if(!root.isConnected)return;const running=s.job?.state==='running';root.querySelector('#ltx-install').disabled=s.installed||running;root.querySelector('#ltx-cancel').disabled=!running;root.querySelector('#ltx-install-state').innerHTML=`

${s.installed?'Installiert · bereit':'Noch nicht installiert'}

Original-Backend: ${e(s.desktop_revision)}
LTX-Inferenz: ${e(s.ltx_revision)}

${s.shared_cuda?'Vorhandene CUDA-/PyTorch-Pakete werden lesend wiederverwendet.':'Eigene CUDA-/PyTorch-Pakete erforderlich.'} Benötigter freier Speicher: ${e(s.required_disk_gib)} GiB. Installation lädt keine Gewichte und startet kein Modell.

${s.job?`

${e(s.job.state)} · ${e(s.job.phase)}

${running?'':''}`:''}`;if(s.job){const log=await api('/log');if(root.isConnected)root.querySelector('#ltx-install-log').textContent=log.text;}}catch(x){root.querySelector('#ltx-install-message').textContent=x.message;}if(root.isConnected)setTimeout(poll,3000);} + for(const [id,path] of [['ltx-install','/install'],['ltx-cancel','/cancel']])root.querySelector('#'+id).onclick=async()=>{try{await api(path,{});root.querySelector('#ltx-install-message').textContent='Aktion angenommen.';poll();}catch(x){root.querySelector('#ltx-install-message').textContent=x.message;}}; + poll();} + return {render}; +})(); diff --git a/ltx_original_entry.py b/ltx_original_entry.py new file mode 100644 index 0000000..d458c55 --- /dev/null +++ b/ltx_original_entry.py @@ -0,0 +1,46 @@ +"""Launch the pinned original API; only deployment/device boundaries are extended.""" +import argparse +import inspect +import json +import logging +import os +from pathlib import Path +import sys + +def install_device_policy(torch,encoder_class,offload_mode): + """Keep native encoding on CUDA1, transfer conditioning to pipeline's CUDA0.""" + original_init=encoder_class.__init__;original_call=encoder_class.__call__ + signature=inspect.signature(original_init) + def init(self,*args,**kwargs): + bound=signature.bind(self,*args,**kwargs) + destination=bound.arguments['device'];bound.arguments['device']=torch.device('cuda:1') + if 'offload_mode' in signature.parameters:bound.arguments['offload_mode']=offload_mode.DISK + original_init(*bound.args,**bound.kwargs);self._deck_output_device=destination + def call(self,*args,**kwargs): + result=original_call(self,*args,**kwargs) + return [type(row)(*(value.to(self._deck_output_device) if hasattr(value,'to') else value for value in row)) for row in result] + encoder_class.__init__=init;encoder_class.__call__=call + +def launch(backend,data,port): + sys.path.insert(0,str(backend));os.environ['LTX_APP_DATA_DIR']=str(data);os.environ['LTX_PORT']=str(port) + os.environ.setdefault('USE_SAGE_ATTENTION','0');os.environ.setdefault('DO_NOT_TRACK','1') + # The original backend can log prompts. Deck deliberately persists no inference logs. + logging.disable(logging.CRITICAL) + import torch + if torch.cuda.device_count()!=2:raise RuntimeError('LTX Original benötigt die ausgewählte 5080 und 3060.') + import services.ltx_pipeline_common as common + from ltx_pipelines.utils.types import OffloadMode + # CUDA CPU-offload would pin the full BF16 transformer in RAM. Disk streaming is + # provided by the original pipelines and bounds host memory for this 32-GiB worker. + common.offload_mode_for_prefetch_count=lambda count,device:OffloadMode.DISK + from ltx_pipelines.utils.blocks import PromptEncoder + install_device_policy(torch,PromptEncoder,OffloadMode) + # Install before Desktop wraps PromptEncoder so its native/API patch delegates + # to the real pinned constructor and cannot obscure offload_mode in **kwargs. + import ltx2_server as original + import uvicorn + uvicorn.run(original.app,host='127.0.0.1',port=port,access_log=False,log_config=None) + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('--backend',required=True);parser.add_argument('--data',required=True);parser.add_argument('--port',type=int,required=True);args=parser.parse_args() + launch(Path(args.backend),Path(args.data),args.port) diff --git a/ltx_original_runtime.py b/ltx_original_runtime.py new file mode 100644 index 0000000..01f9bc5 --- /dev/null +++ b/ltx_original_runtime.py @@ -0,0 +1,105 @@ +"""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).freedeadline: + 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() diff --git a/network/install_remote.py b/network/install_remote.py index ddc1b57..ad8a39a 100644 --- a/network/install_remote.py +++ b/network/install_remote.py @@ -12,7 +12,7 @@ import sys NAME = 'athena-deck-network' BASE = Path('/opt/athena-deck') -FILES = {'backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','image_upload.py','prompt_enhancer.py','prompt_enhancer_worker.py','audio_policy.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','image_runtime.py','image_encoder_node.py','docker_support.py','docker-ui.js','deploy/image-requirements.lock','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py', 'runtime-ui.js', 'video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py', 'catalog-ui.js', 'studio.js', 'auth.py', 'access-ui.js', 'server.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'} +FILES = {'ltx_original_runtime.py','ltx_original_entry.py','video_original.py','ltx-original-ui.js','deploy/ltx-original-requirements.lock','backup.py','backup_codec.py','backup-ui.js','deploy/docker_backup.py','video_comfy.py','video_comfy_node.py','video_comfy_proxy.py','image_upload.py','prompt_enhancer.py','prompt_enhancer_worker.py','audio_policy.py','dashboard_data.py','dashboard_history.py','dashboard-ui.js','dashboard.css','themes.css','deploy/tts-requirements.lock','stt.py','stt-ui.js','api_compat.py','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','image_runtime.py','image_encoder_node.py','docker_support.py','docker-ui.js','deploy/image-requirements.lock','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py', 'runtime-ui.js', 'video.py','video_proxy.py','video-ui.js','catalog.py','hub_auth.py', 'catalog-ui.js', 'studio.js', 'auth.py', 'access-ui.js', 'server.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'} def run(*args, **kwargs): return subprocess.run(args, capture_output=True, timeout=600, **kwargs) diff --git a/server.py b/server.py index 531b92d..b973a9a 100644 --- a/server.py +++ b/server.py @@ -113,8 +113,10 @@ class Server(ThreadingHTTPServer): self.auto_tests=AutoTests(self.catalog.root.parent/'auto-tests.json',self.profiles,self.worker,self.scheduler) def stop_gpu_work(): self.auto_tests.stop();self.chat_tests.stop();self.image_tests.stop();self.prompt_enhancer.stop_preview();self.tts_tests.stop();self.worker.stop() - from video_comfy import VideoComfy - self.video=VideoComfy(self.scheduler,stop_gpu_work,self.docker,self.catalog.root.parent/'video',self.catalog,self.profiles,self.image_runtime) + from video_original import VideoRuntimes + from ltx_original_runtime import LTXOriginalRuntime + self.ltx_original_runtime=LTXOriginalRuntime(self.catalog.root.parent/'ltx-original-runtime',self.image_runtime) + self.video=VideoRuntimes(self.scheduler,stop_gpu_work,self.docker,self.catalog.root.parent/'video',self.catalog,self.profiles,self.image_runtime,original_runtime=self.ltx_original_runtime) self.endpoint.video=self.video self.profiles.video_runtime_ready=lambda:self.image_runtime.status().get("installed",False) from dashboard_data import Dashboard @@ -276,7 +278,7 @@ class Handler(BaseHTTPRequestHandler): return self.headers.get('Host') in allowed def execution_setup(self, model, profile=None): - installed={'stt':bool(self.server.runtime.status().get('active')),'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False),'video':self.server.image_runtime.status().get('installed',False)} + installed={'stt':bool(self.server.runtime.status().get('active')),'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False),'video':self.server.video.status()['runtime']['installed']} return execution_assess(model,installed,profile) def do_GET(self): @@ -290,7 +292,7 @@ class Handler(BaseHTTPRequestHandler): return self.respond({'error':'Anmeldung erforderlich.'},401) if not self.authenticated() and self.path == '/': return self.respond((ROOT/'login.html').read_bytes(), mime='text/html; charset=utf-8') - routes = {'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} + routes = {'/ltx-original-ui.js':('ltx-original-ui.js','text/javascript'),'/backup-ui.js':('backup-ui.js','text/javascript'),'/dashboard-ui.js':('dashboard-ui.js','text/javascript'),'/dashboard.css':('dashboard.css','text/css'),'/themes.css':('themes.css','text/css'),'/video-ui.js':('video-ui.js','text/javascript'),'/stt-ui.js':('stt-ui.js','text/javascript'),'/tts-ui.js':('tts-ui.js','text/javascript'),'/auto-test-ui.js':('auto-test-ui.js','text/javascript'),'/chat-test-ui.js':('chat-test-ui.js','text/javascript'),'/endpoint-ui.js':('endpoint-ui.js','text/javascript'),'/docker-ui.js': ('docker-ui.js','text/javascript'), '/': ('index.html', 'text/html; charset=utf-8'), '/app.js': ('app.js', 'text/javascript'), '/style.css': ('style.css', 'text/css'), '/network-ui.js': ('network-ui.js', 'text/javascript'), '/access-ui.js': ('access-ui.js', 'text/javascript'), '/studio.js': ('studio.js', 'text/javascript'), '/catalog-ui.js': ('catalog-ui.js','text/javascript'), '/runtime-ui.js': ('runtime-ui.js','text/javascript'), '/profiles-ui.js': ('profiles-ui.js','text/javascript'), '/image-test-ui.js': ('image-test-ui.js','text/javascript')} if self.path in routes: name, mime = routes[self.path] return self.respond((ROOT/name).read_bytes(), mime=mime) @@ -309,6 +311,8 @@ class Handler(BaseHTTPRequestHandler): if urlsplit(self.path).path == '/api/v1/tts/audio': try:return self.respond(self.server.tts_tests.audio(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='audio/wav') except (OSError,ValueError):return self.respond({'error':'Audio nicht verfügbar.'},404) + if self.path == '/api/v1/ltx-original-runtime':return self.respond(self.server.ltx_original_runtime.status()) + if self.path == '/api/v1/ltx-original-runtime/log':return self.respond(self.server.ltx_original_runtime.log()) if self.path == '/api/v1/video':return self.respond(self.server.video.status()) if self.path == '/api/v1/image-runtime':return self.respond(self.server.image_runtime.status()) if self.path == '/api/v1/image-tests':return self.respond(self.server.image_tests.status()) @@ -478,9 +482,16 @@ class Handler(BaseHTTPRequestHandler): if data:raise ValueError('Keine Parameter erwartet.') return self.respond(self.server.chat_tests.stop() if self.path.endswith('/cancel') else self.server.chat_tests.unload()) except ValueError as exc:return self.respond({'error':str(exc)},400) - if self.path in ('/api/v1/video/mode','/api/v1/video/service'): + if self.path in ('/api/v1/ltx-original-runtime/install','/api/v1/ltx-original-runtime/cancel'): + try: + if self.read_json():raise ValueError('Keine Installationsparameter erwartet.') + if self.path.endswith('/cancel'):return self.respond(self.server.ltx_original_runtime.stop()) + return self.respond(self.server.ltx_original_runtime.start()) + except (OSError,ValueError) as exc:return self.respond({'error':str(exc)},400) + if self.path in ('/api/v1/video/mode','/api/v1/video/service','/api/v1/video/runtime'): try: data=self.read_json() + if self.path.endswith('/runtime') and set(data)=={'runtime'}:return self.respond(self.server.video.select_runtime(data['runtime'])) if self.path.endswith('/service') and set(data)=={'id'}:return self.respond(self.server.video.select(data['id'])) if set(data)!={'mode'}:raise ValueError('Nur Modus llm oder video erwartet.') return self.respond(self.server.video.switch(data['mode'])) @@ -646,6 +657,7 @@ def main(): server.tts_tests.stop() server.tts_runtime.stop() server.image_runtime.stop() + server.ltx_original_runtime.stop() server.prompt_enhancer.stop() server.image_tests.stop() server.runtime.stop() diff --git a/studio.js b/studio.js index 2b6a2c1..2b6997c 100644 --- a/studio.js +++ b/studio.js @@ -27,7 +27,7 @@ const Studio=(()=>{ else if(section!=='running')CatalogUI.bind(page,section==='library',section==='downloads'); document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);}); } - function runtimeOverview(){return `
EINSTELLUNGEN

Laufzeiten

Laufzeiten werden einmal eingerichtet und für kompatible Modelle wiederverwendet. Entdecken, Bibliothek und Profile zeigen den passenden Einrichtungsweg; unbekannte Modelle werden als noch nicht unterstützt gekennzeichnet. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.

Spracherkennung

Qwen3-ASR mit llama.cpp und Audio-Projektor auf der CPU.

STT einrichten →

Text-to-Speech

Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.

TTS einrichten →

llama.cpp

Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.

llama.cpp öffnen →

Bildgenerierung

Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.

Bildlaufzeit öffnen →

Docker

Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.

Docker öffnen →
`;} + function runtimeOverview(){return `
EINSTELLUNGEN

Laufzeiten

Laufzeiten werden einmal eingerichtet und für kompatible Modelle wiederverwendet. Entdecken, Bibliothek und Profile zeigen den passenden Einrichtungsweg; unbekannte Modelle werden als noch nicht unterstützt gekennzeichnet. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.

Spracherkennung

Qwen3-ASR mit llama.cpp und Audio-Projektor auf der CPU.

STT einrichten →

Text-to-Speech

Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.

TTS einrichten →

llama.cpp

Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.

llama.cpp öffnen →

Bildgenerierung

Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.

Bildlaufzeit öffnen →

LTX Original

Originales LTX-Desktop-Backend mit nativer LTX-API für LTX DeskWEB.

LTX Original einrichten →

Docker

Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.

Docker öffnen →
`;} function imageRuntime(){return `
EINSTELLUNGEN / LAUFZEITEN

Bildgenerierung

ComfyUI + ComfyUI-GGUF

Eigene Bildlaufzeit für Qwen-Image-2.1 GGUF und das unterstützte FLUX.2-Klein-9B-Modell. Die Umgebung kann über den Deck-Installer mitgeliefert oder hier separat installiert werden. Modelle laden erst bei einem Testauftrag.

Textencoder läuft auf der RTX 3060, Diffusionsmodell und VAE auf der RTX 5080. Bei großen FLUX-Gewichten wird zusätzlich System-RAM genutzt. Beide GPUs müssen frei sein. Pro Test wird ein eigener Prozess gestartet und anschließend beendet.

Installationsstatus wird geprüft …

Bildgenerierung → Testen öffnen
${ImageTestUI.html(true)}`;} function openProfile(page,modelId){render(page,true,modelId);} return {render,openProfile}; diff --git a/test_ltx_original.py b/test_ltx_original.py new file mode 100644 index 0000000..330fd15 --- /dev/null +++ b/test_ltx_original.py @@ -0,0 +1,85 @@ +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock,patch +from ltx_original_runtime import LTXOriginalRuntime,DESKTOP_REV +from video_original import VideoRuntimes +from ltx_original_entry import install_device_policy +from test_video_comfy import VideoComfyTests + +class OriginalSelectionTests(VideoComfyTests): + def setUp(self): + super().setUp();self.original=Mock();self.original.status.return_value={'installed':True} + self.video=VideoRuntimes(self.scheduler,Mock(),self.video.helper,self.root/'video',self.catalog,self.profiles,self.runtime,original_runtime=self.original) + def test_missing_comfy_does_not_block_installed_original(self): + self.runtime.paths.return_value=(self.root/'missing',self.root/'missing-comfy') + self.video.select_runtime('original');self.video.select(self.model['id']) + self.assertTrue(self.video.models()[0]['runnable']);self.assertEqual(self.video.status()['runtime_kind'],'original') + saved=json.loads(self.video.path.read_text());self.assertEqual(saved['runtime'],'original');self.assertEqual(saved['model_id'],self.model['id']) + def test_missing_original_is_explicit(self): + self.original.status.return_value={'installed':False};self.video.select_runtime('original') + self.assertFalse(self.video.models()[0]['runnable']);self.assertIn('LTX Original fehlt',self.video.models()[0]['blockers'][-1]) + def test_no_runtime_switch_while_video_running(self): + self.scheduler.gpu_mode='video' + with self.assertRaises(ValueError):self.video.select_runtime('original') + self.assertEqual(self.video.runtime_kind(),'comfy') + def test_reject_unknown_runtime(self): + with self.assertRaises(ValueError):self.video.select_runtime('../backend') + +class InstallerTests(unittest.TestCase): + def test_no_implicit_install_and_no_external_active_path(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);image=Mock();image.paths.return_value=(root/'missing',root/'comfy') + runtime=LTXOriginalRuntime(root,image);self.assertFalse(runtime.status()['installed']) + (root/'active.json').write_text(json.dumps({'id':'../../external'}));self.assertFalse(runtime.status()['installed']) + self.assertEqual(runtime.status()['required_disk_gib'],25) + def test_existing_ready_install_is_reused(self): + with tempfile.TemporaryDirectory() as directory: + root=Path(directory);base=root/('a'*32);(base/'python/bin').mkdir(parents=True);(base/'desktop/backend').mkdir(parents=True) + (base/'python/bin/python').touch();(base/'desktop/backend/ltx2_server.py').touch();(base/'ready').write_text(DESKTOP_REV) + (root/'active.json').write_text(json.dumps({'id':base.name}));image=Mock();image.paths.return_value=(root/'missing',root/'comfy') + runtime=LTXOriginalRuntime(root,image);self.assertTrue(runtime.status()['installed']) + with self.assertRaises(ValueError):runtime.start() + +class DevicePolicyTests(unittest.TestCase): + def test_encoder_gpu_and_conditioning_transfer_preserve_native_contract(self): + from collections import namedtuple + Row=namedtuple('Row','video audio mask') + class Tensor: + def __init__(self,device):self.device=device + def to(self,device):return Tensor(device) + class Encoder: + def __init__(self,model_paths,dtype,device,offload_mode='none'):self.device=device;self.offload=offload_mode + def __call__(self,prompts,**kwargs):return [Row(Tensor(self.device),None,Tensor(self.device))] + torch=Mock();torch.device.side_effect=lambda x:x + modes=Mock();modes.DISK='disk' + install_device_policy(torch,Encoder,modes) + encoder=Encoder('paths','bf16','cuda:0');self.assertEqual(encoder.device,'cuda:1');self.assertEqual(encoder.offload,'disk') + result=encoder(['synthetic prompt'])[0];self.assertEqual(result.video.device,'cuda:0');self.assertIsNone(result.audio);self.assertEqual(result.mask.device,'cuda:0') + +class ProxyContractTests(unittest.TestCase): + def test_native_paths_and_errors_are_forwarded_without_schema_adapter(self): + import http.client + import threading + from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer + from video_comfy_proxy import relay + class Native(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def do_POST(self): + raw=self.rfile.read(int(self.headers.get('Content-Length',0))) + self.server.received=(self.path,raw) + self.send_response(422);self.send_header('Content-Type','application/json');self.end_headers();self.wfile.write(b'{"detail":"native validation"}') + native=ThreadingHTTPServer(('127.0.0.1',0),Native) + class Forward(BaseHTTPRequestHandler): + def log_message(self,*args):pass + def do_POST(self):relay(self,native.server_port) + forward=ThreadingHTTPServer(('127.0.0.1',0),Forward) + for server in (native,forward):threading.Thread(target=server.serve_forever,daemon=True).start() + try: + client=http.client.HTTPConnection('127.0.0.1',forward.server_port);body=b'{"prompt":"synthetic","imagePath":"/synthetic/input.jpg"}' + client.request('POST','/api/generate?contract=1',body,{'Content-Type':'application/json'}) + response=client.getresponse();self.assertEqual(response.status,422);self.assertEqual(response.read(),b'{"detail":"native validation"}');client.close() + self.assertEqual(native.received,('/api/generate?contract=1',body)) + finally: + for server in (forward,native):server.shutdown();server.server_close() diff --git a/video-ui.js b/video-ui.js index def2851..a36ef6b 100644 --- a/video-ui.js +++ b/video-ui.js @@ -3,20 +3,21 @@ window.VideoUI=(()=>{ async function api(path,data){const r=await fetch('/api/v1/'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const s=await r.json();if(!r.ok)throw Error(s.error||'Anfrage fehlgeschlagen');return s;} function mode(root,s,profiles=[],port=null){ if(root.dataset.busy==='1')return; - root.innerHTML=`

GPU-Modus

Gemeinsamer API-Port: ${e(port||"siehe Endpunkt")} · LLM-Modus: OpenAI-API · Video-Modus: ComfyUI-API und Weboberfläche

${e(s.service?.name||'Kein Videodienst eingerichtet')}

${s.state==='switching'?`
${e(s.switch_phase)}
`:`

${s.mode==='llm'?'LLM-Betrieb · Videodienst gestoppt':s.service?.ready?'Video-API bereit':s.service?.running?'Videodienst läuft · API noch nicht bereit':'Dienststatus nicht prüfbar · GPU-Aufträge gesperrt'}

`}${s.service?.api_url?`

Backend: ${e(s.service.api_url)}

${e(s.service.message)}

${s.service.ready&&port?`ComfyUI öffnen →`:""}`:''}${s.error?``:''}

Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.

Aktiven Videodienst im Bereich Video wählen
`; + root.innerHTML=`

GPU-Modus

Gemeinsamer API-Port: ${e(port||"siehe Endpunkt")} · LLM-Modus: OpenAI-API · Video-Modus: ${s.runtime_kind==='original'?'originale LTX-API':'ComfyUI-API'}

${e(s.service?.name||'Kein Videodienst eingerichtet')}

${s.state==='switching'?`
${e(s.switch_phase)}
`:`

${s.mode==='llm'?'LLM-Betrieb · Videodienst gestoppt':s.service?.ready?'Video-API bereit':s.service?.running?'Videodienst läuft · API noch nicht bereit':'Dienststatus nicht prüfbar · GPU-Aufträge gesperrt'}

`}${s.service?.api_url?`

Backend: ${e(s.service.api_url)}

${e(s.service.message)}

${s.service.ready&&port?`${s.runtime_kind==='original'?'LTX DeskWEB öffnen':'ComfyUI öffnen'} →`:""}`:''}${s.error?``:''}

Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.

Aktiven Videodienst im Bereich Video wählen
`; root.querySelectorAll('[data-mode]').forEach(b=>b.onclick=async()=>{root.dataset.busy='1';root.querySelectorAll('button').forEach(x=>x.disabled=true);root.querySelector('.video-message').textContent='Moduswechsel wird angefordert …';try{const next=await api('video/mode',{mode:b.dataset.mode});root.dataset.busy='0';mode(root,next,profiles,port);}catch(x){root.querySelector('.video-message').textContent=x.message;root.dataset.busy='0';}}); } function services(root){ if(!root||root.dataset.bound)return;root.dataset.bound='1'; async function poll(){if(!root.isConnected)return;try{const s=await api('video');if(root.dataset.busy!=='1'){ - root.innerHTML=`

Gemeinsame ComfyUI-Laufzeit

${s.runtime?.installed?'Vorhanden · wird mit Bildgenerierung geteilt':'Nicht installiert · unter Einstellungen → Bildgenerierung einrichten'}

5080: Diffusionsmodell und VAE. 3060: Textencoder. Große BF16-Dateien nutzen zusätzlich CPU-Auslagerung. Gewichte laden erst bei einer Workflow-Anfrage.

${(s.models||[]).map(x=>`

${e(x.model.file)}

${e(x.label)}${x.blockers.map(b=>`

${e(b)}

`).join('')}

${Object.keys(x.components).length} Komponenten geprüft · gleiche Quellversion

${x.model.id===s.selected&&s.mode==='llm'?'':''}
`).join('')||'

Kein unterstütztes LTX-Modell in der Bibliothek. Unter Entdecken herunterladen.

'}

`; + root.innerHTML=`

Video-Laufzeit

${s.runtime?.installed?'Vorhanden · bereit':s.runtime_kind==='original'?'Nicht installiert · unter Einstellungen → LTX Original einrichten':'Nicht installiert · unter Einstellungen → Bildgenerierung einrichten'}

5080: Diffusionsmodell und VAE. 3060: Textencoder. Große BF16-Dateien nutzen zusätzlich CPU-Auslagerung. Gewichte laden erst bei einer Anfrage.

LTX Original installieren / Status →
${(s.models||[]).map(x=>`

${e(x.model.file)}

${e(x.label)}${x.blockers.map(b=>`

${e(b)}

`).join('')}

${Object.keys(x.components).length} Komponenten geprüft · gleiche Quellversion

${x.model.id===s.selected&&s.mode==='llm'?'':''}
`).join('')||'

Kein unterstütztes LTX-Modell in der Bibliothek. Unter Entdecken herunterladen.

'}

`; + root.querySelector('#video-runtime-choice').onchange=async event=>{root.dataset.busy='1';try{await api('video/runtime',{runtime:event.target.value});}catch(x){root.querySelector('.video-message').textContent=x.message;}finally{root.dataset.busy='0';poll();}}; root.querySelectorAll('[data-components-model]').forEach(b=>b.onclick=async()=>{root.dataset.busy='1';try{const data=await api('profiles/components?'+new URLSearchParams({model_id:b.dataset.componentsModel}));root.innerHTML=`

LTX-Komponenten

${e(data.message)}

${data.requirements.map((r,i)=>`

${e(r.label)} · ${r.available.length?'Bereits heruntergeladen':r.candidates.length?'Fehlt':'In dieser Quellversion nicht gefunden'}

${!r.available.length?r.candidates.map((c,j)=>``).join(''):''}`).join('')}

`;root.querySelector('[data-close-components]').onclick=()=>{root.dataset.busy='0';poll();};root.querySelectorAll('[data-download-component]').forEach(d=>d.onclick=async()=>{const [i,j]=d.dataset.downloadComponent.split(':');const f=data.requirements[i].candidates[j];d.disabled=true;try{await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:'video'});root.querySelector('.video-message').textContent='Download eingeplant. Fortschritt unter Downloads. Nach Abschluss werden die Komponenten automatisch erkannt.';}catch(x){root.querySelector('.video-message').textContent=x.message;d.disabled=false;}});}catch(x){root.dataset.busy='0';root.querySelector('.video-message').textContent=x.message;}}); root.querySelectorAll('[data-select]').forEach(b=>b.onclick=async()=>{root.dataset.busy='1';try{await api('video/service',{id:b.dataset.select});root.querySelector('.video-message').textContent='Videomodell ausgewählt. Start über die Übersicht.';}catch(x){root.querySelector('.video-message').textContent=x.message;}finally{root.dataset.busy='0';}}); }}catch(x){root.textContent=x.message;}if(root.isConnected)setTimeout(poll,5000);}poll(); } function runtime(){ const view=document.querySelector('#view');if(view.querySelector('#video-service-page'))return; - view.innerHTML='

Video

Die vorhandene ComfyUI-Laufzeit wird wiederverwendet. Deck prüft Modellgewichte und Komponenten; Generierungsparameter gehören in die ComfyUI-Oberfläche.

'; + view.innerHTML='

Video

LTX Original mit LTX DeskWEB oder die vorhandene ComfyUI-Laufzeit auswählen. Deck verwaltet Modelle, Komponenten und den GPU-Modus; Generierungsparameter gehören in die jeweilige Video-Oberfläche.

'; function show(tab){const root=view.querySelector('#video-content');if(tab==='runtime'){root.innerHTML='
';services(root.firstChild);}else{root.innerHTML=CatalogUI.html(tab==='library',tab==='downloads');CatalogUI.bind('video',tab==='library',tab==='downloads');}} view.querySelectorAll('[data-video-tab]').forEach(b=>b.onclick=()=>show(b.dataset.videoTab));show('runtime'); } diff --git a/video_original.py b/video_original.py new file mode 100644 index 0000000..cf7a702 --- /dev/null +++ b/video_original.py @@ -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()