Show video transition and generation progress in separate views
This commit is contained in:
@@ -168,10 +168,12 @@ Eigene Video-API auf demselben API-Port und mit demselben Bearer-Token wie Chat
|
||||
- `GET /v1/videos/{id}`: Zustand/Phase und Fehler des letzten Auftrags, ohne Prompt.
|
||||
- `GET /v1/videos/{id}/content`: fertige MP4 einschließlich Audiospur.
|
||||
|
||||
Anfragewerte überschreiben Profilstandards. Breite/Höhe sind Vielfache von 64, 256–1920 bzw. 256–1088; Bildanzahl 9–241 in der Form 8n+1, FPS 1–60. Distilled hat eine feste Schrittfolge; das bisherige allgemeine Schritte-Profilfeld wird von diesem Adapter nicht verwendet. Resultate liegen im Deck-Zustandsverzeichnis `video/`; der erste Adapter bietet jeweils den letzten Auftrag an. Kein Prompt wird auf Platte geschrieben. Eigener Video-Test mit Vorschau/Download unter Laufzeiten → Video. Bild-/Audio-Upload, automatische Video-Tool-Aufträge aus Hermes, Abfragehistorie und automatische Ergebnisbereinigung sind noch nicht umgesetzt.
|
||||
Anfragewerte überschreiben Profilstandards. Breite/Höhe sind Vielfache von 64, 256–1920 bzw. 256–1088; Bildanzahl 9–241 in der Form 8n+1, FPS 1–60. Distilled hat eine feste Schrittfolge; das bisherige allgemeine Schritte-Profilfeld wird von diesem Adapter nicht verwendet. Resultate liegen im Deck-Zustandsverzeichnis `video/`; der erste Adapter bietet jeweils den letzten Auftrag an. Kein Prompt wird auf Platte geschrieben. Eigener Video-Test mit Vorschau/Download unter Video → Testen. Bild-/Audio-Upload, automatische Video-Tool-Aufträge aus Hermes, Abfragehistorie und automatische Ergebnisbereinigung sind noch nicht umgesetzt.
|
||||
|
||||
Während Video/Moduswechsel erhalten neue Chat-, Bild- und TTS-API-Aufträge HTTP 503 mit `video_mode_active`. CPU-STT bleibt verfügbar. Ein Wechsel wird nur über die angemeldete Verwaltungsoberfläche ausgelöst; API-Clients dürfen den Modus nicht heimlich zurückschalten. Die interne Verwaltungs-API verwendet `GET /api/v1/video`, `POST /api/v1/video/profile` mit `{id}`, `POST /api/v1/video/mode` mit `{mode:"llm"|"video"}`, `/api/v1/video/generate` und `/api/v1/video-runtime/install|cancel`; jeweils bestehender Sitzungsschutz und POST-Header `X-Athena-Deck: 1`.
|
||||
|
||||
Video-Build-Voraussetzung: Python-Entwicklungsheader passend zur verwendeten Python-Version sowie ein C-Compiler (Debian: `python3-dev`, `build-essential`). Das Docker-Installationspaket bringt diese mit. Triton kompiliert seinen CUDA-Helfer beim ersten Auftrag; dafür werden keine Host-Treiber installiert.
|
||||
|
||||
Verifiziert auf Athena am 29.09.2026: offizielles LTX-2.5-Distilled-BF16-Profil mit zugeordneten Komponenten vorbereitet; exklusiver Modus sperrt Chat mit HTTP 503 / `video_mode_active`; synthetischer POST-Videoauftrag liefert HTTP 202, Statusabfrage und MP4-Download funktionieren. Ergebnis: 256×256, 9 dekodierbare Bilder, 24 FPS und Audiospur (12.802 Bytes). Nach Rückwechsel auf LLM war der Video-Prozess beendet und der GPU-Speicher wieder auf Treibergrundbelegung (3060: 1 MiB, 5080: 6 MiB). Der anfängliche Triton-Kompilierfehler wurde durch Python-Entwicklungsheader im Deck-Image behoben. Kein Produktivdienst wurde für diese Prüfung verändert. Dieser kleine Funktionstest ist keine Speicher- oder Geschwindigkeitsgarantie für größere Auflösungen/Längen.
|
||||
|
||||
Video-Oberfläche: Moduswechsel erfolgen ausschließlich in der Übersicht. Eine laufende Ladeanzeige zeigt Phase und verstrichene Zeit. Video → Testen meldet einen falschen Modus mit Link zur Übersicht und bestätigt das Absenden sofort. Video → Laufend zeigt ausschließlich Worker, aktuellen Auftrag und Ergebnis. Installationsaktionen liegen unter Einstellungen → Laufzeiten → Video. Die Auftragsanzeige wird alle 1,5 Sekunden aktualisiert; der Worker liefert Phasen, jedoch keine belastbaren Prozentwerte.
|
||||
|
||||
@@ -7,7 +7,7 @@ const Studio=(()=>{
|
||||
if(!force&&document.querySelector('#studio')?.dataset.page===page)return;
|
||||
if(category!==page){category=page;section='discover';}
|
||||
if(modelId)section='profiles';
|
||||
const body=page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}</p>${audioDescriptions[page]&&!['audio','stt'].includes(page)?'<p class="note">Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.</p>':''}<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt','video'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${['test','running'].includes(section)&&page==='video'?VideoUI.html():section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`;
|
||||
const body=page==='stt-runtime'?STTUI.html(true):page==='tts-runtime'?TTSUI.html(true):['docker-runtime','services'].includes(page)?DockerUI.html(page==='services'):page==='runtimes'?runtimeOverview():page==='image-runtime'?imageRuntime():page==='runtime'?RuntimeUI.html():`<div class="kicker">ATHENA / MODELLVERWALTUNG</div><h1>${labels[page]}</h1><p>${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}</p>${audioDescriptions[page]&&!['audio','stt'].includes(page)?'<p class="note">Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.</p>':''}<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],...(['audio','stt'].includes(page)?[['setup','Einrichten']]:[]),...(['image','chat','audio','stt','video'].includes(page)?[['test','Testen'],...(page==='chat'?[['auto','Auto-Test']]:[])]:[]),['running','Laufend']].map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-section="${id}">${label}</button>`).join('')}</div><div role="tabpanel">${['test','running'].includes(section)&&page==='video'?VideoUI.html(section==='running'?'running':'test'):section==='setup'?(page==='stt'?STTUI.html(true):TTSUI.html(true)):['test','running'].includes(section)&&page==='stt'?STTUI.html():['test','running'].includes(section)&&page==='audio'?TTSUI.html():section==='auto'?AutoTestUI.html():section==='test'?(page==='chat'?ChatTestUI.html():ImageTestUI.html()):section==='running'&&page==='image'?ImageTestUI.html(true):section==='profiles'?ProfilesUI.html():section==='running'&&page==='chat'?EndpointUI.runningHTML():section==='running'?'<section class="card"><h2>Keine von Deck gestarteten Modelle</h2><p>Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.</p><a class="link" href="#runtime">llama.cpp-Builds verwalten →</a></section>':CatalogUI.html(section==='library',section==='downloads')}</div>`;
|
||||
document.querySelector('#view').innerHTML=`<div id="studio" data-page="${page}">${body}</div>`;
|
||||
if(['docker-runtime','services'].includes(page)){DockerUI.bind(page==='services');return;}
|
||||
if(page==='stt-runtime'){STTUI.bind(true);return;}
|
||||
|
||||
@@ -22,3 +22,7 @@ nav a.nav-sub{padding-left:28px;font-size:12px;color:#a4bda8}.component-card{mar
|
||||
.chat-text,#chat-transcript pre{white-space:pre-wrap;overflow-wrap:anywhere;}
|
||||
|
||||
.endpoint-choice{display:flex;align-items:center;gap:.6rem}.endpoint-choice input{width:auto;margin:0}
|
||||
|
||||
.video-activity,.video-notice{padding:18px;border:1px solid #536a51;border-radius:10px;margin:16px 0;background:#202c25}.video-activity strong{display:block}.video-activity progress{display:block;width:100%;height:12px;margin:14px 0;accent-color:#c5edaf}.video-error{color:#ffd5bf;border-left:3px solid #d99b73;padding-left:14px}.video-test-fields{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:16px}#video-test textarea{width:100%;box-sizing:border-box;background:#101718;color:#e4eee5;border:1px solid #465348;border-radius:8px;padding:14px;font:inherit;resize:vertical}.generated-video{display:block;max-width:100%;margin:18px 0;border-radius:8px}
|
||||
|
||||
.video-activity progress:indeterminate{background:linear-gradient(90deg,#303a37 0%,#c5edaf 50%,#303a37 100%);background-size:200% 100%;animation:video-progress 1.8s linear infinite;border:0;border-radius:8px}.video-activity progress:indeterminate::-webkit-progress-bar{background:transparent}@keyframes video-progress{from{background-position:200% 0}to{background-position:0 0}}@media(prefers-reduced-motion:reduce){.video-activity progress:indeterminate{animation:none}}
|
||||
|
||||
+62
-15
@@ -1,27 +1,74 @@
|
||||
window.VideoUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const labels={llm:'LLM',video:'Video',switching:'Wechsel läuft',idle:'Bereit für LLM',ready:'Videoprofil bereit',failed:'Fehlgeschlagen',running:'Video wird erzeugt',complete:'Video fertig'};
|
||||
const elapsed=t=>{const seconds=Math.max(0,Math.floor(Date.now()/1000-t));return `${Math.floor(seconds/60)} Min. ${seconds%60} Sek.`;};
|
||||
const busy=(title,detail='')=>`<div class="video-activity" role="status" aria-live="polite" aria-busy="true"><strong>${e(title)}</strong><progress aria-label="Verarbeitung läuft"></progress><p>${e(detail)}</p></div>`;
|
||||
async function api(path,data){const r=await fetch('/api/v1/'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{}),s=await r.json();if(!r.ok)throw Error(s.error||'Anfrage fehlgeschlagen');return s;}
|
||||
function workerState(s){
|
||||
if(s.state==='switching')return busy(`Wechsel zu ${labels[s.target_mode]||'anderem Modus'}`,`${s.switch_phase||'Laufende Aufträge beenden und Speicher freigeben'}${s.switch_started_at?' · '+elapsed(s.switch_started_at):''}`);
|
||||
return `<div class="video-notice ${s.error?'video-error':''}" role="status"><strong>${e(labels[s.state]||s.state)}</strong><p>GPU-Modus: ${e(labels[s.mode]||s.mode)}${s.profile_name?' · Geladen: '+e(s.profile_name):''}</p>${s.error?`<p role="alert">${e(s.error)}</p>`:''}</div>`;
|
||||
}
|
||||
function jobState(job){
|
||||
if(!job)return '<p>Noch kein Video-Test gestartet.</p>';
|
||||
if(job.state==='running')return busy('Video-Test läuft',`${job.phase||'Auftrag wird vorbereitet'} · Laufzeit ${elapsed(job.created_at)}. BF16 mit Disk-Streaming kann mehrere Minuten benötigen. Eine verlässliche Prozentanzeige liefert der Worker noch nicht.`);
|
||||
return `<div class="video-notice ${job.state==='failed'?'video-error':''}" role="status"><strong>${e(labels[job.state]||job.state)}</strong>${job.state==='complete'?'<p>Das Ergebnis steht unten zur Vorschau und zum Download bereit.</p>':''}${job.error?`<p role="alert">${e(job.error)}</p>`:''}</div>`;
|
||||
}
|
||||
function mode(root,s,profiles){
|
||||
if(root.dataset.busy==='1')return;
|
||||
const html=`<section class="card"><h2>GPU-Modus</h2><div class="card-actions"><button data-mode="llm" ${s.mode==='llm'&&s.state!=='failed'||s.state==='switching'?'disabled':''}>LLM</button><button data-mode="video" ${s.mode==='video'&&s.state==='ready'||s.state==='switching'?'disabled':''}>Video</button></div><p>Modus: <strong>${e(s.mode.toUpperCase())}</strong> · ${e(s.state)} · ${e(s.error||'')}</p><p>Freigegebenes Videoprofil: <strong>${e(profiles.find(p=>p.id===s.profile_id)?.name||'Keines')}</strong> · <a class="link" href="#video">Unter Video → Profile freigeben</a></p><p>${e(s.memory_policy)}</p><p>Video beendet laufende Deck-GPU-Aufträge und sperrt Chat, Bild und TTS (HTTP 503). CPU-STT bleibt verfügbar. Fremde Dienste werden nicht beendet.</p><p><a class="link" href="#video-runtime">Video-Laufzeit und Testauftrag →</a></p><p class="video-message" role="status"></p></section>`;
|
||||
const html=`<section class="card"><h2>GPU-Modus</h2><div class="card-actions"><button data-mode="llm" ${s.mode==='llm'&&s.state!=='failed'||s.state==='switching'?'disabled':''}>LLM</button><button data-mode="video" ${s.mode==='video'&&s.state==='ready'||s.state==='switching'?'disabled':''}>Video</button></div>${workerState(s)}<p>Freigegebenes Videoprofil: <strong>${e(profiles.find(p=>p.id===s.profile_id)?.name||'Keines')}</strong> · <a class="link" href="#video">Unter Video → Profile freigeben</a></p><p>${e(s.memory_policy)}</p><p>Video beendet laufende Deck-GPU-Aufträge und sperrt Chat, Bild und TTS. CPU-STT bleibt verfügbar.</p><div class="video-message" role="status"></div></section>`;
|
||||
if(root._html===html)return;root._html=html;root.innerHTML=html;
|
||||
async function act(path,data){root.dataset.busy='1';try{await api(path,data);root._html='';root.querySelector('.video-message').textContent='Übernommen; Status wird aktualisiert.';}catch(x){root.querySelector('.video-message').textContent=x.message;}finally{root.dataset.busy='0';}}
|
||||
root.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>act('video/mode',{mode:b.dataset.mode}));
|
||||
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').innerHTML=busy('Moduswechsel wird angefordert …');
|
||||
let next;
|
||||
try{next=await api('video/mode',{mode:b.dataset.mode});}catch(x){root.querySelector('.video-message').textContent=x.message;root.querySelectorAll('button').forEach(x=>x.disabled=false);}
|
||||
finally{root.dataset.busy='0';root._html='';}
|
||||
if(next&&root.isConnected)mode(root,next,profiles);
|
||||
});
|
||||
}
|
||||
function html(testing=false){return `<section id="video-runtime-page">${testing?'<h2>Video testen</h2><p>Unter Video → Profile ein Profil freigeben, dann hier den Video-Modus aktivieren und einen kurzen Test starten.</p><div id="video-test-mode"></div>':'<h1>LTX-Video-Laufzeit</h1>'}<div ${testing?'hidden':''}><p>Eigene Python-Umgebung, gepinnte LTX-Pipelines, CUDA 12.8. Installation verändert keine Host-Treiber. Erster Ausführungsweg: Text → Video mit dem Distilled-BF16-Rezept und Disk-Streaming auf der RTX 5080.</p><p id="video-status"></p><button id="video-install">Laufzeit installieren</button><button id="video-cancel">Installation abbrechen</button><p><a href="#home">Modus und aktives Profil in der Übersicht auswählen →</a></p></div><p id="video-message" role="status"></p><section class="card"><h2>Video-Test</h2><p>Zuerst den Video-Modus aktivieren. Werte überschreiben die Profilstandards für diesen Auftrag. Distilled nutzt seine feste Schrittfolge. Noch kein Bild-/Audio-Upload.</p><form id="video-test"><label>Prompt<textarea name="prompt" required maxlength="8000"></textarea></label>${[['width','Breite',256],['height','Höhe',256],['frames','Bilder (8n+1)',9],['fps','FPS',24],['seed','Seed',42]].map(([n,l,v])=>`<label>${l}<input type="number" name="${n}" value="${v}" required></label>`).join('')}<button>Video erzeugen</button></form><p id="video-job"></p><div id="video-result"></div><p>API: POST /v1/videos · GET /v1/videos/{id} · GET /v1/videos/{id}/content. Gleicher Port und API-Token wie Chat; eigenständige Deck-Video-API.</p></section></section>`;}
|
||||
async function runtime(){
|
||||
const view=document.querySelector('#view');if(view.querySelector('#video-runtime-page'))return;
|
||||
view.innerHTML=html();bind(view.firstElementChild);
|
||||
function html(view='test'){
|
||||
const test=view==='test',runtime=view==='runtime';
|
||||
return `<section id="video-runtime-page" data-video-view="${view}"><h2>${runtime?'LTX-Video-Laufzeit':test?'Video testen':'Video · Laufend'}</h2>${runtime?'<p>Eigene LTX-Python-Umgebung mit CUDA und Disk-Streaming auf der RTX 5080.</p><div id="video-install-status"></div><button id="video-install">Laufzeit installieren</button><button id="video-cancel">Installation abbrechen</button>':'<div id="video-worker-state"></div>'}<div id="video-message" role="status" aria-live="polite"></div>${test?`<section class="card"><p>Kurzer Text-zu-Video-Test mit dem freigegebenen Profil. Werte gelten nur für diesen Auftrag. Noch kein Bild-/Audio-Upload.</p><p id="video-mode-hint"></p><form id="video-test"><label>Prompt<textarea name="prompt" required maxlength="8000" rows="4" placeholder="Zum Beispiel: Ein roter Ball auf einem Holztisch, ruhige Kamera."></textarea></label><div class="video-test-fields">${[['width','Breite',256,256,1920,64],['height','Höhe',256,256,1088,64],['frames','Bilder (8n+1)',9,9,241,8],['fps','FPS',24,1,60,1],['seed','Seed',42,-1,2147483647,1]].map(([n,l,v,min,max,step])=>`<label>${l}<input type="number" name="${n}" value="${v}" min="${min}" max="${max}" step="${step}" required></label>`).join('')}</div><button id="video-generate" disabled>Video erzeugen</button></form></section>`:''}${!runtime?'<section class="card"><h3>Auftragsstatus</h3><div id="video-job" aria-live="polite"></div><div id="video-result"></div></section>':'<p>Video → Testen bietet einen kurzen Funktionstest; Video → Laufend zeigt den Worker und den aktuellen Auftrag.</p>'}</section>`;
|
||||
}
|
||||
function runtime(){const view=document.querySelector('#view');if(view.querySelector('[data-video-view="runtime"]'))return;view.innerHTML=html('runtime');bind(view.firstElementChild);}
|
||||
function bind(root=document.querySelector('#video-runtime-page')){
|
||||
if(!root)return;
|
||||
const msg=root.querySelector('#video-message');
|
||||
async function action(path,data){try{await api(path,data);msg.textContent='Auftrag angenommen.';}catch(x){msg.textContent=x.message;}}
|
||||
root.querySelector('#video-install').onclick=()=>action('video-runtime/install',{});
|
||||
root.querySelector('#video-cancel').onclick=()=>action('video-runtime/cancel',{});
|
||||
root.querySelector('#video-test button').disabled=true;
|
||||
root.querySelector('#video-test').onsubmit=ev=>{ev.preventDefault();const d=Object.fromEntries(new FormData(ev.target));for(const k of ['width','height','frames','fps','seed'])d[k]=Number(d[k]);action('video/generate',d);};
|
||||
async function poll(){if(!root.isConnected)return;try{const s=await api('video');if(!root.isConnected)return;const panel=root.querySelector('#video-test-mode');if(panel){const p=await api('profiles');if(!root.isConnected)return;mode(panel,s,p.profiles);}root.querySelector('#video-status').textContent=s.runtime.installed?'Installiert · '+s.runtime.revision:(s.runtime.job?.phase||'Noch nicht installiert');root.querySelector('#video-install').disabled=s.runtime.installed||s.runtime.job?.state==='running';root.querySelector('#video-cancel').disabled=s.runtime.job?.state!=='running';root.querySelector('#video-test button').disabled=s.mode!=='video'||s.state!=='ready'||s.job?.state==='running';root.querySelector('#video-job').textContent=s.job?`${s.job.state} · ${s.job.phase||''} · ${s.job.id} · ${s.job.error||''}`:'Noch kein Video-Auftrag.';if(s.job?.state==='complete'&&root.querySelector('#video-result').dataset.id!==s.job.id){root.querySelector('#video-result').dataset.id=s.job.id;root.querySelector('#video-result').innerHTML=`<video controls preload="metadata" style="max-width:100%" src="/api/v1/video/content?id=${e(s.job.id)}"></video><a href="/api/v1/video/content?id=${e(s.job.id)}" download="athena-video.mp4">Video herunterladen</a>`;}}catch(x){msg.textContent=x.message;}if(root.isConnected)setTimeout(poll,2000);}poll();
|
||||
const msg=root.querySelector('#video-message'),form=root.querySelector('#video-test');let current=null,pending=false,revision=0,pollError=false;
|
||||
function draw(s){
|
||||
current=s;
|
||||
const worker=root.querySelector('#video-worker-state');if(worker)worker.innerHTML=workerState(s);
|
||||
const ready=s.mode==='video'&&s.state==='ready',running=s.job?.state==='running';
|
||||
if(form){
|
||||
const hint=root.querySelector('#video-mode-hint');hint.innerHTML=ready?'Videomodus bereit. Du kannst einen Test starten.':'Für diesen Test muss der Video-Modus bereit sein. Bitte in der <a class="link" href="#home">Übersicht auf Video umschalten</a>.';
|
||||
form.querySelector('button').disabled=pending||running||s.state==='switching';
|
||||
form.querySelector('button').textContent=pending?'Wird gestartet …':running?'Test läuft …':'Video erzeugen';
|
||||
form.setAttribute('aria-busy',String(pending||running));
|
||||
}
|
||||
const job=root.querySelector('#video-job');if(job)job.innerHTML=jobState(s.job);
|
||||
const result=root.querySelector('#video-result');
|
||||
if(result&&s.job?.state==='complete'&&result.dataset.id!==s.job.id){result.dataset.id=s.job.id;result.innerHTML=`<video controls preload="metadata" class="generated-video" src="/api/v1/video/content?id=${e(s.job.id)}"></video><a class="link" href="/api/v1/video/content?id=${e(s.job.id)}" download="athena-video.mp4">Video herunterladen</a>`;}
|
||||
else if(result&&s.job?.state!=='complete'){result.replaceChildren();delete result.dataset.id;}
|
||||
const installation=root.querySelector('#video-install-status');
|
||||
if(installation){const r=s.runtime;installation.innerHTML=r.job?.state==='running'?busy('Video-Laufzeit wird installiert',r.job.phase):`<p>${e(r.installed?'Installiert':r.job?.phase||'Noch nicht installiert')}</p>`;root.querySelector('#video-install').disabled=pending||r.installed||r.job?.state==='running';root.querySelector('#video-cancel').disabled=pending||r.job?.state!=='running';}
|
||||
}
|
||||
async function action(path,data){
|
||||
if(pending)return;
|
||||
if(path==='video/generate'&&(!current||current.mode!=='video'||current.state!=='ready')){msg.innerHTML='<p class="video-error">Falscher Modus oder Video noch nicht bereit. Bitte zuerst in der <a class="link" href="#home">Übersicht auf Video umschalten</a>.</p>';return;}
|
||||
pending=true;revision++;root.querySelectorAll('button').forEach(b=>b.disabled=true);
|
||||
msg.innerHTML=busy(path==='video/generate'?'Video-Test wird gestartet …':'Anfrage wird verarbeitet …');
|
||||
try{const value=await api(path,data);if(!root.isConnected)return;
|
||||
if(path==='video/generate'){current={...current,job:value};msg.textContent='Test gestartet. Der Fortschritt steht unter Auftragsstatus.';root.querySelector('#video-result').replaceChildren();draw(current);}else {current={...current,runtime:value};msg.textContent='Auftrag angenommen.';}
|
||||
}catch(x){if(root.isConnected)msg.innerHTML=`<p class="video-error" role="alert">${e(x.message)}</p>`;}
|
||||
finally{pending=false;revision++;if(root.isConnected&¤t)draw(current);}
|
||||
}
|
||||
root.querySelector('#video-install')?.addEventListener('click',()=>action('video-runtime/install',{}));
|
||||
root.querySelector('#video-cancel')?.addEventListener('click',()=>action('video-runtime/cancel',{}));
|
||||
if(form)form.onsubmit=ev=>{ev.preventDefault();const d=Object.fromEntries(new FormData(form));for(const k of ['width','height','frames','fps','seed'])d[k]=Number(d[k]);action('video/generate',d);};
|
||||
async function poll(){if(!root.isConnected)return;const version=revision;
|
||||
try{const s=await api('video');if(root.isConnected&&!pending&&version===revision){if(pollError){msg.textContent='Verbindung wiederhergestellt.';pollError=false;}draw(s);}}
|
||||
catch(x){if(root.isConnected&&!pending&&version===revision){pollError=true;msg.innerHTML=`<p class="video-error">Status momentan nicht erreichbar: ${e(x.message)}. Ein laufender Auftrag kann weiterlaufen.</p>`;current=null;if(form)form.querySelector('button').disabled=true;}}
|
||||
if(root.isConnected)setTimeout(poll,1500);
|
||||
}poll();
|
||||
}
|
||||
return {mode,runtime,html:()=>html(true),bind};
|
||||
return {mode,runtime,html,bind};
|
||||
})();
|
||||
|
||||
@@ -17,12 +17,12 @@ class Video:
|
||||
def __init__(self,root,profiles,runtime,scheduler,stop_owned):
|
||||
self.root=Path(root);self.profiles=profiles;self.runtime=runtime;self.scheduler=scheduler;self.stop_owned=stop_owned
|
||||
self.lock=threading.RLock();self.mode='llm';self.state='idle';self.error=None;self.process=None;self.profile=None;self.job=None;self.thread=None;self.gpu=None
|
||||
self.closing=False;self.selected=None
|
||||
self.switch_phase=None;self.switch_started_at=None;self.target_mode=None;self.closing=False;self.selected=None
|
||||
if (self.root/'selection.json').exists():self.selected=json.loads((self.root/'selection.json').read_text()).get('profile_id')
|
||||
def status(self):
|
||||
with self.lock:
|
||||
if self.process and self.process.poll() is not None and self.state=='ready':self.state='failed';self.error='Video-Worker beendet; RAM-OOM oder Laufzeitfehler möglich.'
|
||||
return dict(mode=self.mode,state=self.state,error=self.error,profile_name=self.profile['name'] if self.profile else None,profile_id=self.selected,loaded_profile_id=self.profile['id'] if self.profile else None,gpu=self.gpu,job=dict(self.job) if self.job else None,runtime=self.runtime.status(),memory_policy='Disk-Streaming auf RTX 5080; beide GPUs exklusiv reserviert. Gewichte werden bedarfsgerecht geladen.')
|
||||
return dict(target_mode=self.target_mode,switch_phase=self.switch_phase,switch_started_at=self.switch_started_at,mode=self.mode,state=self.state,error=self.error,profile_name=self.profile['name'] if self.profile else None,profile_id=self.selected,loaded_profile_id=self.profile['id'] if self.profile else None,gpu=self.gpu,job=dict(self.job) if self.job else None,runtime=self.runtime.status(),memory_policy='Disk-Streaming auf RTX 5080; beide GPUs exklusiv reserviert. Gewichte werden bedarfsgerecht geladen.')
|
||||
def blockers(self,p):
|
||||
if not video_recipe(p.get('model') or {}):return ['Für diese Videovariante ist kein Worker angebunden.']
|
||||
errors=[]
|
||||
@@ -53,12 +53,13 @@ class Video:
|
||||
if errors:raise ValueError(' '.join(errors))
|
||||
# Close admission before stopping any process. Queued leases recheck this flag.
|
||||
with self.scheduler.cv:self.scheduler.gpu_mode='switching';self.scheduler.cv.notify_all()
|
||||
self.state='switching';self.error=None
|
||||
self.state='switching';self.error=None;self.target_mode=mode;self.switch_started_at=time.time();self.switch_phase='Laufende Deck-Aufträge beenden'
|
||||
self.thread=threading.Thread(target=self._switch,args=(mode,profile),daemon=True);self.thread.start()
|
||||
return self.status()
|
||||
def _switch(self,mode,profile):
|
||||
try:
|
||||
self.stop_owned();self.stop_process()
|
||||
with self.lock:self.switch_phase='Auf Freigabe des GPU-Speichers warten'
|
||||
deadline=time.monotonic()+60
|
||||
with self.scheduler.cv:
|
||||
while self.scheduler.active or self.scheduler.transition:
|
||||
@@ -66,7 +67,9 @@ class Video:
|
||||
self.scheduler.cv.wait(.2)
|
||||
self.scheduler.key=None
|
||||
if self.closing:raise ValueError('Deck wird beendet.')
|
||||
if mode=='video':self.load(profile)
|
||||
if mode=='video':
|
||||
with self.lock:self.switch_phase='GPUs prüfen und Videoprofil vorbereiten'
|
||||
self.load(profile)
|
||||
if self.closing:raise ValueError('Deck wird beendet.')
|
||||
with self.lock:self.mode=mode;self.state='ready' if mode=='video' else 'idle'
|
||||
with self.scheduler.cv:self.scheduler.gpu_mode=mode;self.scheduler.cv.notify_all()
|
||||
@@ -133,6 +136,7 @@ class Video:
|
||||
p=subprocess.Popen([str(python),str(Path(__file__).with_name('video_worker.py'))],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.DEVNULL,bufsize=0,start_new_session=True,env=env)
|
||||
with self.lock:self.process=p;self.gpu=gpu['uuid']
|
||||
if self.closing:self.stop_process();raise ValueError('Deck wird beendet.')
|
||||
with self.lock:self.switch_phase='Video-Worker laden und Komponenten prüfen'
|
||||
p.stdin.write((json.dumps(config)+'\n').encode());p.stdin.flush();result=self.read(p,180)
|
||||
if result.get('state')!='ready':raise ValueError(result.get('error','Video-Profil nicht bereit.'))
|
||||
with self.lock:self.profile=profile
|
||||
|
||||
Reference in New Issue
Block a user