Compare commits
4
Commits
e7a1deaab0
...
fb78259668
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb78259668 | ||
|
|
21f11eac57 | ||
|
|
ea33527233 | ||
|
|
a6b7ab93ab |
@@ -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.
|
||||
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
# 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/<id>` 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
|
||||
|
||||
Installation auf Athena, Import-/Abhängigkeitsprüfung und der Wechsel über die
|
||||
Deck-Oberfläche wurden durchgeführt. DeskWEB verwendet die neuen Medien-Mounts;
|
||||
sein authentifizierter Proxy erreicht die originale API. Ein synthetischer
|
||||
Bild-Upload wurde auf dem gemeinsamen Datenträger geprüft.
|
||||
|
||||
Echte CUDA-Generierung: LTX 2.5 Fast, 540p, fünf Sekunden, 24 FPS, Seed 42,
|
||||
ohne Audio; die originale API lieferte HTTP 200 und eine MP4-Datei. Textencoding
|
||||
lief auf der 3060, Diffusion auf der 5080. Nach Beenden des Testprozesses wurden
|
||||
die GPU-Allokationen freigegeben. Optionalfunktionen sind damit nicht geprüft.
|
||||
Die gezielten 21 Gerätepolicy-/Runtime-/Transporttests sind erfolgreich.
|
||||
|
||||
Der erste Versuch fand ein volles 256-MiB-Container-Temp-Verzeichnis: CUDA-,
|
||||
Triton- und Torch-Caches sowie temporäre Dateien liegen nun unter
|
||||
`video/original-work`, auf dem Datenträger statt im kleinen tmpfs.
|
||||
Der Docker-Helfer erhält sein Socket-Verzeichnis über Dienstneustarts hinweg,
|
||||
damit dessen Verzeichnis-Mount im Deck-Testcontainer gültig bleibt.
|
||||
@@ -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.
|
||||
|
||||
@@ -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;}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ window.BackupUI=(()=>{
|
||||
view.querySelector('#backup-import').onsubmit=async e=>{e.preventDefault();if(pending)return;pending=true;message.textContent='Backup entschlüsseln und Quellen prüfen …';try{const file=view.querySelector('#backup-file').files[0];if(!file||file.size>64*1024**2)throw Error('Backup-Datei fehlt oder ist größer als 64 MiB.');const r=await fetch('/api/v1/backup/inspect',{method:'POST',headers:{'Content-Type':'application/octet-stream','X-Athena-Deck':'1','X-Backup-Passphrase':btoa(String.fromCharCode(...new TextEncoder().encode(view.querySelector('#backup-import-password').value)))},body:file});const data=await r.json();if(!r.ok)throw Error(data.error);preview=data;view.querySelector('#backup-import-password').value='';showPreview(panel,data,message);message.textContent='Backup geprüft. Details und Zusatzdienste auswählen.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
|
||||
poll(view);
|
||||
}
|
||||
function showPreview(panel,p,message){panel.innerHTML=`<h3>Wiederherstellungsplan</h3><p>${p.profiles.length} Profile · ${p.models.length} Modelldateien · ${size(p.download_bytes)} fehlende Gewichte · ${p.wireguard?'WireGuard enthalten':'Kein Deck-WireGuard'}</p><details><summary>Modelle und Profile anzeigen</summary><ul>${p.models.map(m=>`<li>${esc(m.name)} · ${size(m.bytes)} · ${m.present?'vorhanden, wird geprüft':'wird heruntergeladen'}</li>`).join('')}</ul><p>${p.profiles.map(esc).join(' · ')}</p></details><h3>Zusatzdienste</h3>${p.services.length?p.services.map(c=>`<label><input type="checkbox" class="backup-service" value="${esc(c.name)}" ${c.restorable?'checked':'disabled'}> ${esc(c.name)} · ${esc(c.registry||c.image)} ${c.build_recipe?'· wird aus festem Rezept gebaut':''}${!c.restorable?'· keine Wiederherstellungsquelle':''}</label>`).join(''):'<p>Keine zugeordneten Container.</p>'}<h3>Laufzeiten</h3><p>llama.cpp: ${esc(p.runtimes.llama?.commit||'nicht eingerichtet')} · ComfyUI: ${p.runtimes.image?'vorhandene wiederverwenden / fehlende installieren':'nicht benötigt'} · TTS: ${p.runtimes.tts?'wiederherstellen':'nicht benötigt'}</p>${p.warnings.map(w=>`<p class="note">${esc(w)}</p>`).join('')}${p.blockers.map(w=>`<p role="alert">${esc(w)}</p>`).join('')}<label><input type="checkbox" id="backup-credentials" checked> Oberflächenkennwort und API-Zugang aus Backup wiederherstellen</label><p class="small">Danach gilt das bisherige Kennwort des Backups. Der aktuelle Zugang bleibt erhalten, wenn der Haken entfernt wird.</p><label><input type="checkbox" id="backup-confirm"> Einstellungen ersetzen und ausgewählte Downloads/Builds automatisch ausführen. Lokaler Rückfallstand wird vorher gesichert.</label><p><button id="backup-restore" ${p.blockers.length?'disabled':''}>Wiederherstellung starten</button></p>`;
|
||||
function showPreview(panel,p,message){panel.innerHTML=`<h3>Wiederherstellungsplan</h3><p>${p.profiles.length} Profile · ${p.models.length} Modelldateien · ${size(p.download_bytes)} fehlende Gewichte · ${p.wireguard?'WireGuard enthalten':'Kein Deck-WireGuard'}</p><details><summary>Modelle und Profile anzeigen</summary><ul>${p.models.map(m=>`<li>${esc(m.name)} · ${size(m.bytes)} · ${m.present?'vorhanden, wird geprüft':'wird heruntergeladen'}</li>`).join('')}</ul><p>${p.profiles.map(esc).join(' · ')}</p></details><h3>Zusatzdienste</h3>${p.services.length?p.services.map(c=>`<label><input type="checkbox" class="backup-service" value="${esc(c.name)}" ${c.restorable?'checked':'disabled'}> ${esc(c.name)} · ${esc(c.registry||c.image)} ${c.build_recipe?'· wird aus festem Rezept gebaut':''}${!c.restorable?'· keine Wiederherstellungsquelle':''}</label>`).join(''):'<p>Keine zugeordneten Container.</p>'}<h3>Laufzeiten</h3><p>llama.cpp: ${esc(p.runtimes.llama?.commit||'nicht eingerichtet')} · ComfyUI: ${p.runtimes.image?'vorhandene wiederverwenden / fehlende installieren':'nicht benötigt'} · TTS: ${p.runtimes.tts?'wiederherstellen':'nicht benötigt'} · LTX Original: ${p.runtimes.ltx_original?'wiederherstellen':'nicht benötigt'}</p>${p.warnings.map(w=>`<p class="note">${esc(w)}</p>`).join('')}${p.blockers.map(w=>`<p role="alert">${esc(w)}</p>`).join('')}<label><input type="checkbox" id="backup-credentials" checked> Oberflächenkennwort und API-Zugang aus Backup wiederherstellen</label><p class="small">Danach gilt das bisherige Kennwort des Backups. Der aktuelle Zugang bleibt erhalten, wenn der Haken entfernt wird.</p><label><input type="checkbox" id="backup-confirm"> Einstellungen ersetzen und ausgewählte Downloads/Builds automatisch ausführen. Lokaler Rückfallstand wird vorher gesichert.</label><p><button id="backup-restore" ${p.blockers.length?'disabled':''}>Wiederherstellung starten</button></p>`;
|
||||
panel.querySelector('#backup-restore').onclick=async()=>{if(pending)return;if(!panel.querySelector('#backup-confirm').checked){message.textContent='Bitte den angezeigten Wiederherstellungsplan bestätigen.';return;}pending=true;try{await request('restore',{id:p.id,services:[...panel.querySelectorAll('.backup-service:checked')].map(x=>x.value),confirm:true,restore_credentials:panel.querySelector('#backup-credentials').checked});panel.innerHTML='';preview=null;message.textContent='Wiederherstellung gestartet.';}catch(e){message.textContent=e.message;}finally{pending=false;}};
|
||||
}
|
||||
async function poll(view){if(!view.querySelector('#backup-page'))return;const panel=view.querySelector('#backup-progress');try{const r=await fetch('/api/v1/backup');if(!r.ok)throw Error('Restore-Status nicht erreichbar.');const {job,preview:p,checkpoints=[]}=await r.json();const saved=view.querySelector('#backup-checkpoints');saved.innerHTML='<h2>Lokale Rückfallstände</h2>'+checkpoints.map(name=>`<p><button data-checkpoint="${esc(name)}">${esc(name)} prüfen</button></p>`).join('');saved.querySelectorAll('[data-checkpoint]').forEach(b=>b.onclick=async()=>{try{const r=await request('checkpoint',{name:b.dataset.checkpoint});const p=await r.json();preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}catch(e){view.querySelector('#backup-message').textContent=e.message;}});if(p&&!preview){preview=p;showPreview(view.querySelector('#backup-preview'),p,view.querySelector('#backup-message'));}if(!job){panel.innerHTML='<h2>Wiederherstellung</h2><p>Noch kein Restore gestartet.</p>';}else{const running=job.state==='running';panel.innerHTML=`<h2>${job.state==='complete'?'Alles wiederhergestellt':job.state==='partial'?'Wiederherstellung mit offenen Punkten':'Wiederherstellung · '+esc(job.state)}</h2><p>${esc(job.phase)}</p><progress max="${job.total}" value="${job.completed}"></progress><p>${job.completed}/${job.total} Schritte</p>${job.current?.total?`<progress max="${job.current.total}" value="${job.current.bytes||0}"></progress><p>${size(job.current.bytes||0)} / ${size(job.current.total)}${job.current.bytes_per_second?' · '+(job.current.bytes_per_second/1024**2).toFixed(1)+' MiB/s':''}${job.current.eta_seconds!=null?' · noch etwa '+Math.ceil(job.current.eta_seconds/60)+' Minuten':''}</p>`:''}${running?'<button id="backup-cancel">Wiederherstellung abbrechen</button>':''}<ul>${job.items.map(x=>`<li><strong>${esc(x.name)}</strong> · ${esc(x.state)}<br>${esc(x.message)}</li>`).join('')}</ul>`;if(running)panel.querySelector('#backup-cancel').onclick=async()=>{try{await request('cancel',{});view.querySelector('#backup-message').textContent='Abbruch angefordert; laufender Container-Schritt wird sicher beendet.';}catch(e){view.querySelector('#backup-message').textContent=e.message;}};if(job.state==='complete'&&jobId!==job.id){jobId=job.id;applyDeckTheme(job.theme);}}}catch(e){view.querySelector('#backup-message').textContent=e.message;}setTimeout(()=>poll(view),1500);}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+3
-2
@@ -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 \
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -89,7 +89,9 @@ def validate(c):
|
||||
targets.add(target)
|
||||
if m.get('index')!=i or type(m.get('read_only')) is not bool or type(m.get('was_file')) is not bool:raise ValueError('Ungültige Volume-Einstellung.')
|
||||
rel=m.get('deck_path')
|
||||
if rel is not None and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Dateien dürfen nur als bekannte, lesende Modell-/Token-Volumes eingebunden werden.')
|
||||
media={'video/original-work/remote-inputs/deskweb':('/data/inputs',False),'video/original-work/outputs':('/data/outputs',True)}
|
||||
native_media=rel in media and c['name']=='ltx-deskweb' and (target,m['read_only'])==media[rel] and not m['was_file']
|
||||
if rel is not None and not native_media and (rel not in ('models','video/comfy-work/models','video/comfy-client-token') or not m['read_only']):raise ValueError('Deck-Volume nicht freigegeben; nur bekannte Modell-/Tokenpfade und LTX-Medienordner erlaubt.')
|
||||
for name,data in m.get('files',{}).items():
|
||||
if name not in ('file','Settings.fds','Backends.fds'):raise ValueError('Unbekannte Konfigurationsdatei.')
|
||||
if not isinstance(data,str) or len(base64.b64decode(data,validate=True))>MAX_FILE:raise ValueError('Ungültige Konfigurationsdatei.')
|
||||
@@ -150,7 +152,10 @@ def restore(manager,c):
|
||||
if m['deck_path']=='video/comfy-client-token' and not source.exists():
|
||||
import secrets
|
||||
source.parent.mkdir(parents=True,exist_ok=True);source.write_text(secrets.token_urlsafe(48)+'\n');source.chmod(0o600);os.chown(source,65534,65534)
|
||||
elif m['deck_path']!='video/comfy-client-token':source.mkdir(parents=True,exist_ok=True)
|
||||
elif m['deck_path']!='video/comfy-client-token':
|
||||
source.mkdir(parents=True,exist_ok=True)
|
||||
if m['deck_path'].startswith('video/original-work/'):
|
||||
is_input=m['deck_path'].endswith('/deskweb');os.chown(source,1000 if is_input else 65534,65534);source.chmod(0o2770 if is_input else 0o2750)
|
||||
elif m['was_file']:
|
||||
if 'file' not in m['files']:raise ValueError('Benötigte Volume-Datei fehlt im Backup.')
|
||||
source.write_bytes(base64.b64decode(m['files']['file']));source.chmod(0o600)
|
||||
|
||||
+2
-2
@@ -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):
|
||||
@@ -204,7 +204,7 @@ def update(base, rollback=False, force=False, api_ports=None, reuse_runtime=Fals
|
||||
backup_name=config['name']+'-previous'
|
||||
if inspect(backup_name):raise RuntimeError('Rückfall-Containername belegt. Keine Änderung ausgeführt.')
|
||||
stamp=str(time.time_ns())
|
||||
shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*'))
|
||||
shutil.copytree(base/'state',base/'backups'/stamp,ignore=shutil.ignore_patterns('ltx-original-runtime','original-work','tts-runtime','tts-tests','models','runtime','runtime-verification','image-tests','image-verification','image-verification-*','image-runtime','video-runtime','video-verification','video-verification-*','comfy-work','router-verification-*'))
|
||||
was_running=previous['State']['Running']
|
||||
if was_running:run('docker','stop','--time','15',config['name'])
|
||||
run('docker','rename',config['name'],backup_name)
|
||||
|
||||
@@ -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
|
||||
@@ -41,6 +41,7 @@ ExecStart={command}
|
||||
Restart=on-failure
|
||||
RuntimeDirectory=athena-deck-docker
|
||||
RuntimeDirectoryMode=0755
|
||||
RuntimeDirectoryPreserve=yes
|
||||
StateDirectory=athena-deck-docker
|
||||
StateDirectoryMode=0700
|
||||
UMask=0077
|
||||
|
||||
+1
-1
@@ -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':
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/dashboard.css"><link rel="stylesheet" href="/themes.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/stt-ui.js" defer></script><script src="/tts-ui.js" defer></script><script src="/studio.js" defer></script><script src="/video-ui.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/dashboard-ui.js" defer></script><script src="/backup-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#dashboard"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#dashboard">Dashboard <span>01</span></a><a href="#home">Übersicht <span>02</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><p class="nav-heading">AUDIO</p><a class="nav-sub" href="#audio">Text-to-Speech (TTS)</a><a class="nav-sub" href="#stt">Spracherkennung (STT)</a><a class="nav-sub" href="#music">Musik</a><a class="nav-sub" href="#voice">Stimme / Voice</a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#backup">Backup & Restore <span>↗</span></a><a href="#appearance">Darstellung <span>↗</span></a><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#stt-runtime">Spracherkennung</a><a class="nav-sub" href="#tts-runtime">Text-to-Speech</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt & Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang & API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
|
||||
<!doctype html><html lang="de"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Athena Deck</title><link rel="stylesheet" href="/style.css"><link rel="stylesheet" href="/dashboard.css"><link rel="stylesheet" href="/themes.css"><script src="/network-ui.js" defer></script><script src="/access-ui.js" defer></script><script src="/catalog-ui.js" defer></script><script src="/runtime-ui.js" defer></script><script src="/profiles-ui.js" defer></script><script src="/auto-test-ui.js" defer></script><script src="/chat-test-ui.js" defer></script><script src="/image-test-ui.js" defer></script><script src="/docker-ui.js" defer></script><script src="/stt-ui.js" defer></script><script src="/tts-ui.js" defer></script><script src="/studio.js" defer></script><script src="/ltx-original-ui.js" defer></script><script src="/video-ui.js" defer></script><script src="/endpoint-ui.js" defer></script><script src="/dashboard-ui.js" defer></script><script src="/backup-ui.js" defer></script><script src="/app.js" defer></script></head><body><aside><a class="brand" href="#dashboard"><span>Α</span> ATHENA <b>DECK</b></a><p class="eyebrow">WORKSPACE / ATHENA</p><nav aria-label="Hauptnavigation"><a href="#dashboard">Dashboard <span>01</span></a><a href="#home">Übersicht <span>02</span></a><a href="#chat">Sprachmodelle / Chat <span>02</span></a><a href="#image">Bildgenerierung <span>03</span></a><p class="nav-heading">AUDIO</p><a class="nav-sub" href="#audio">Text-to-Speech (TTS)</a><a class="nav-sub" href="#stt">Spracherkennung (STT)</a><a class="nav-sub" href="#music">Musik</a><a class="nav-sub" href="#voice">Stimme / Voice</a><a href="#video">Video <span>05</span></a><a href="#services">Weitere Dienste <span>06</span></a><p class="nav-heading">EINSTELLUNGEN</p><a href="#backup">Backup & Restore <span>↗</span></a><a href="#appearance">Darstellung <span>↗</span></a><a href="#runtimes">Laufzeiten <span>↗</span></a><a class="nav-sub" href="#runtime">llama.cpp</a><a class="nav-sub" href="#image-runtime">Bildgenerierung</a><a class="nav-sub" href="#stt-runtime">Spracherkennung</a><a class="nav-sub" href="#tts-runtime">Text-to-Speech</a><a class="nav-sub" href="#ltx-original-runtime">LTX Original</a><a class="nav-sub" href="#docker-runtime">Docker</a><a href="#endpoint">Endpunkt & Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang & API <span>↗</span></a></nav><footer><i></i> Isolierte Entwicklungsumgebung<p>Eigener Modell-Router<br>Hardware · nur lesend</p></footer></aside><main><header><span>ATHENA CONTROL SURFACE</span><span class="pill">v0.7 · Router</span></header><div id="view"></div><p id="error" role="alert"></p></main></body></html>
|
||||
|
||||
@@ -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='<div id="ltx-original-page"><h1>LTX Original</h1><p>Originales Lightricks-Backend für die LTX-Desktop-API. LTX DeskWEB bleibt die separate Oberfläche unter Weitere Dienste.</p><section class="card"><h2>Installation</h2><div id="ltx-install-state"></div><button id="ltx-install">LTX Original herunterladen & installieren</button><button id="ltx-cancel" class="secondary">Installation abbrechen</button><p id="ltx-install-message" role="status"></p><details><summary>Eigene Installationsausgabe</summary><pre id="ltx-install-log" style="white-space:pre-wrap;max-height:320px;overflow:auto"></pre></details></section><section class="card"><h2>Verwendung</h2><p>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.</p><p>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.</p><a href="#video">Videomodell und Laufzeit wählen →</a></section></div>';
|
||||
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=`<p><strong>${s.installed?'Installiert · bereit':'Noch nicht installiert'}</strong></p><p>Original-Backend: <code>${e(s.desktop_revision)}</code><br>LTX-Inferenz: <code>${e(s.ltx_revision)}</code></p><p>${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.</p>${s.job?`<p>${e(s.job.state)} · ${e(s.job.phase)}</p>${running?'<progress aria-label="LTX-Installation läuft"></progress>':''}`:''}`;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};
|
||||
})();
|
||||
@@ -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)
|
||||
@@ -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).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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>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.</p><div class="grid"><section class="card"><h2>Spracherkennung</h2><p>Qwen3-ASR mit llama.cpp und Audio-Projektor auf der CPU.</p><a class="link" href="#stt-runtime">STT einrichten →</a></section><section class="card"><h2>Text-to-Speech</h2><p>Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.</p><a class="link" href="#tts-runtime">TTS einrichten →</a></section><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
||||
function runtimeOverview(){return `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>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.</p><div class="grid"><section class="card"><h2>Spracherkennung</h2><p>Qwen3-ASR mit llama.cpp und Audio-Projektor auf der CPU.</p><a class="link" href="#stt-runtime">STT einrichten →</a></section><section class="card"><h2>Text-to-Speech</h2><p>Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.</p><a class="link" href="#tts-runtime">TTS einrichten →</a></section><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>LTX Original</h2><p>Originales LTX-Desktop-Backend mit nativer LTX-API für LTX DeskWEB.</p><a class="link" href="#ltx-original-runtime">LTX Original einrichten →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
||||
function imageRuntime(){return `<div class="kicker">EINSTELLUNGEN / LAUFZEITEN</div><h1>Bildgenerierung</h1><section class="card"><h2>ComfyUI + ComfyUI-GGUF</h2><p>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.</p><p>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.</p><div id="image-runtime-install"><p>Installationsstatus wird geprüft …</p></div><a class="link" href="#image">Bildgenerierung → Testen öffnen</a></section>${ImageTestUI.html(true)}`;}
|
||||
function openProfile(page,modelId){render(page,true,modelId);}
|
||||
return {render,openProfile};
|
||||
|
||||
@@ -82,6 +82,11 @@ class ContainerTests(unittest.TestCase):
|
||||
for change in (lambda x:x.update(unsupported=True),lambda x:x.update(network='host'),lambda x:x['ports']['8000/tcp'][0].update(HostIp='0.0.0.0'),lambda x:x.update(mounts=[dict(index=0,target='/var/run/docker.sock',deck_path=None,read_only=True,was_file=True,files={})])):
|
||||
bad=copy.deepcopy(c);change(bad)
|
||||
with self.assertRaises(ValueError):validate_container(bad)
|
||||
def test_only_deskweb_can_mount_its_specific_writable_media_folder(self):
|
||||
c=self.config();c.update(name='ltx-deskweb',mounts=[dict(index=0,target='/data/inputs',deck_path='video/original-work/remote-inputs/deskweb',read_only=False,was_file=False,files={})]);validate_container(c)
|
||||
for change in (lambda x:x.update(name='other-ui'),lambda x:x['mounts'][0].update(deck_path='models'),lambda x:x['mounts'][0].update(deck_path='video/original-work'),lambda x:x['mounts'][0].update(target='/data/outputs')):
|
||||
bad=copy.deepcopy(c);change(bad)
|
||||
with self.assertRaises(ValueError):validate_container(bad)
|
||||
def test_never_replaces_existing_foreign_container(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
manager=Mock(state=Path(d));(Path(d)/'backup-policy.json').write_text(json.dumps({'deck_state':d}))
|
||||
|
||||
@@ -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()
|
||||
+4
-3
@@ -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=`<section class="card"><h2>GPU-Modus</h2><p>Gemeinsamer API-Port: <strong>${e(port||"siehe Endpunkt")}</strong> · LLM-Modus: OpenAI-API · Video-Modus: ComfyUI-API und Weboberfläche</p><button data-mode="llm" ${s.mode==='llm'||s.state==='switching'?'disabled':''}>LLM</button><button data-mode="video" ${!s.service?.id||s.mode==='video'||s.state==='switching'?'disabled':''}>Video</button><h3>${e(s.service?.name||'Kein Videodienst eingerichtet')}</h3>${s.state==='switching'?`<div class="video-activity" role="status"><strong>${e(s.switch_phase)}</strong><progress></progress></div>`:`<p>${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'}</p>`}${s.service?.api_url?`<p>Backend: <code>${e(s.service.api_url)}</code></p><p>${e(s.service.message)}</p>${s.service.ready&&port?`<a class="link" target="_blank" rel="noopener" href="${e(location.protocol+"//"+location.hostname+":"+port+"/")}">ComfyUI öffnen →</a>`:""}`:''}${s.error?`<p class="video-error" role="alert">${e(s.error)}</p>`:''}<p>Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.</p><a href="#video">Aktiven Videodienst im Bereich Video wählen</a><div class="video-message" role="status"></div></section>`;
|
||||
root.innerHTML=`<section class="card"><h2>GPU-Modus</h2><p>Gemeinsamer API-Port: <strong>${e(port||"siehe Endpunkt")}</strong> · LLM-Modus: OpenAI-API · Video-Modus: ${s.runtime_kind==='original'?'originale LTX-API':'ComfyUI-API'}</p><button data-mode="llm" ${s.mode==='llm'||s.state==='switching'?'disabled':''}>LLM</button><button data-mode="video" ${!s.service?.id||s.mode==='video'||s.state==='switching'?'disabled':''}>Video</button><h3>${e(s.service?.name||'Kein Videodienst eingerichtet')}</h3>${s.state==='switching'?`<div class="video-activity" role="status"><strong>${e(s.switch_phase)}</strong><progress></progress></div>`:`<p>${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'}</p>`}${s.service?.api_url?`<p>Backend: <code>${e(s.service.api_url)}</code></p><p>${e(s.service.message)}</p>${s.service.ready&&port?`<a class="link" target="_blank" rel="noopener" href="${e(location.protocol+"//"+location.hostname+":"+(s.runtime_kind==='original'?8118:port)+"/")}">${s.runtime_kind==='original'?'LTX DeskWEB öffnen':'ComfyUI öffnen'} →</a>`:""}`:''}${s.error?`<p class="video-error" role="alert">${e(s.error)}</p>`:''}<p>Modelle, Auflösung, Dauer und Generierung werden in der jeweiligen Video-Anwendung verwaltet. Video beendet Deck-GPU-Aufträge; fremde Dienste werden nicht beendet.</p><a href="#video">Aktiven Videodienst im Bereich Video wählen</a><div class="video-message" role="status"></div></section>`;
|
||||
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=`<section class="card"><h2>Gemeinsame ComfyUI-Laufzeit</h2><p>${s.runtime?.installed?'Vorhanden · wird mit Bildgenerierung geteilt':'Nicht installiert · unter Einstellungen → Bildgenerierung einrichten'}</p><p>5080: Diffusionsmodell und VAE. 3060: Textencoder. Große BF16-Dateien nutzen zusätzlich CPU-Auslagerung. Gewichte laden erst bei einer Workflow-Anfrage.</p></section>${(s.models||[]).map(x=>`<section class="card"><h3>${e(x.model.file)}</h3><strong>${e(x.label)}</strong><button class="secondary" data-components-model="${e(x.model.id)}">Komponenten prüfen / nachladen</button>${x.blockers.map(b=>`<p class="warning">${e(b)}</p>`).join('')}<p>${Object.keys(x.components).length} Komponenten geprüft · gleiche Quellversion</p><button data-select="${e(x.model.id)}" ${s.mode!=='llm'||x.model.id===s.selected?'disabled':''}>${x.model.id===s.selected?'Aktives Videomodell':'Als Videomodell aktivieren'}</button>${x.model.id===s.selected&&s.mode==='llm'?'<button data-select="">Auswahl aufheben</button>':''}</section>`).join('')||'<p>Kein unterstütztes LTX-Modell in der Bibliothek. Unter Entdecken herunterladen.</p>'}<p class="video-message" role="status"></p>`;
|
||||
root.innerHTML=`<section class="card"><h2>Video-Laufzeit</h2><label>Laufzeit auswählen<select id="video-runtime-choice" ${s.mode!=='llm'?'disabled':''}><option value="comfy" ${s.runtime_kind!=='original'?'selected':''}>ComfyUI · gemeinsam mit Bildgenerierung</option><option value="original" ${s.runtime_kind==='original'?'selected':''}>LTX Original · LTX DeskWEB</option></select></label><p>${s.runtime?.installed?'Vorhanden · bereit':s.runtime_kind==='original'?'Nicht installiert · unter Einstellungen → LTX Original einrichten':'Nicht installiert · unter Einstellungen → Bildgenerierung einrichten'}</p><p>5080: Diffusionsmodell und VAE. 3060: Textencoder. Große BF16-Dateien nutzen zusätzlich CPU-Auslagerung. Gewichte laden erst bei einer Anfrage.</p><a href="#ltx-original-runtime">LTX Original installieren / Status →</a></section>${(s.models||[]).map(x=>`<section class="card"><h3>${e(x.model.file)}</h3><strong>${e(x.label)}</strong><button class="secondary" data-components-model="${e(x.model.id)}">Komponenten prüfen / nachladen</button>${x.blockers.map(b=>`<p class="warning">${e(b)}</p>`).join('')}<p>${Object.keys(x.components).length} Komponenten geprüft · gleiche Quellversion</p><button data-select="${e(x.model.id)}" ${s.mode!=='llm'||x.model.id===s.selected?'disabled':''}>${x.model.id===s.selected?'Aktives Videomodell':'Als Videomodell aktivieren'}</button>${x.model.id===s.selected&&s.mode==='llm'?'<button data-select="">Auswahl aufheben</button>':''}</section>`).join('')||'<p>Kein unterstütztes LTX-Modell in der Bibliothek. Unter Entdecken herunterladen.</p>'}<p class="video-message" role="status"></p>`;
|
||||
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=`<section class="card"><h2>LTX-Komponenten</h2><p>${e(data.message)}</p>${data.requirements.map((r,i)=>`<p><strong>${e(r.label)}</strong> · ${r.available.length?'Bereits heruntergeladen':r.candidates.length?'Fehlt':'In dieser Quellversion nicht gefunden'}</p>${!r.available.length?r.candidates.map((c,j)=>`<button data-download-component="${i}:${j}">${e(c.name)} herunterladen</button>`).join(''):''}`).join('')}<p class="video-message"></p><button data-close-components>Zurück</button></section>`;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='<section id="video-service-page"><h1>Video</h1><p>Die vorhandene ComfyUI-Laufzeit wird wiederverwendet. Deck prüft Modellgewichte und Komponenten; Generierungsparameter gehören in die ComfyUI-Oberfläche.</p><div class="studio-tabs"><button data-video-tab="runtime">Laufzeit / Aktives Modell</button><button data-video-tab="discover">Entdecken</button><button data-video-tab="library">Bibliothek</button><button data-video-tab="downloads">Downloads</button></div><div id="video-content"></div></section>';
|
||||
view.innerHTML='<section id="video-service-page"><h1>Video</h1><p>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.</p><div class="studio-tabs"><button data-video-tab="runtime">Laufzeit / Aktives Modell</button><button data-video-tab="discover">Entdecken</button><button data-video-tab="library">Bibliothek</button><button data-video-tab="downloads">Downloads</button></div><div id="video-content"></div></section>';
|
||||
function show(tab){const root=view.querySelector('#video-content');if(tab==='runtime'){root.innerHTML='<div id="video-services"></div>';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');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
"""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]
|
||||
for name in ('temp','cuda-cache','triton-cache','torch-cache'):(work/name).mkdir(exist_ok=True)
|
||||
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',TMPDIR=str(work/'temp'),CUDA_CACHE_PATH=str(work/'cuda-cache'),TRITON_CACHE_DIR=str(work/'triton-cache'),TORCHINDUCTOR_CACHE_DIR=str(work/'torch-cache'))
|
||||
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