Compare commits

..
4 Commits
24 changed files with 535 additions and 29 deletions

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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.
+8
View File
@@ -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.
+1
View File
@@ -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
View File
@@ -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);}
+18 -7
View File
@@ -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
View File
@@ -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 \
+3 -2
View File
@@ -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 \
+7 -2
View File
@@ -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
View File
@@ -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)
+24
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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 &amp; 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 &amp; Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang &amp; 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 &amp; 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 &amp; Profile <span>↗</span></a><a href="#network">Netzwerk <span>↗</span></a><a href="#access">Zugang &amp; 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>
+11
View File
@@ -0,0 +1,11 @@
window.LTXOriginalUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[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 &amp; 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};
})();
+46
View File
@@ -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)
+105
View File
@@ -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()
+1 -1
View File
@@ -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)
+17 -5
View File
@@ -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()
+1 -1
View File
@@ -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};
+5
View File
@@ -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}))
+85
View File
@@ -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
View File
@@ -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');
}
+75
View File
@@ -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.')