From 0ec51bcf6d94d0005092d46fe8144e45177f32bb Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 29 Sep 2026 13:46:05 +0200 Subject: [PATCH] Show video transition and generation progress in separate views --- README.md | 4 ++- studio.js | 2 +- style.css | 4 +++ video-ui.js | 77 ++++++++++++++++++++++++++++++++++++++++++----------- video.py | 12 ++++++--- 5 files changed, 78 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3f59c49..a05f958 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/studio.js b/studio.js index 9e01848..c9eedad 100644 --- a/studio.js +++ b/studio.js @@ -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():`
${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}
${audioDescriptions[page]&&!['audio','stt'].includes(page)?'Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
':''}Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.
llama.cpp-Builds verwalten →${audioDescriptions[page]||'Modelle entdecken, herunterladen und mit gespeicherten Profilen konfigurieren.'}
${audioDescriptions[page]&&!['audio','stt'].includes(page)?'Entdecken und Downloads sind verfügbar. Für diese Audio-Bereiche ist noch keine ausführbare Laufzeit in Deck angebunden. Gespeicherte Profile sind vorbereitend.
':''}Profile werden auf Athena gespeichert. Für diesen Bereich ist noch kein Modellworker angebunden. Fehlende Komponenten stehen beim jeweiligen Profil.
llama.cpp-Builds verwalten →${e(detail)}
GPU-Modus: ${e(labels[s.mode]||s.mode)}${s.profile_name?' · Geladen: '+e(s.profile_name):''}
${s.error?`${e(s.error)}
`:''}Noch kein Video-Test gestartet.
'; + 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 `Das Ergebnis steht unten zur Vorschau und zum Download bereit.
':''}${job.error?`${e(job.error)}
`:''}Modus: ${e(s.mode.toUpperCase())} · ${e(s.state)} · ${e(s.error||'')}
Freigegebenes Videoprofil: ${e(profiles.find(p=>p.id===s.profile_id)?.name||'Keines')} · Unter Video → Profile freigeben
${e(s.memory_policy)}
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.
Freigegebenes Videoprofil: ${e(profiles.find(p=>p.id===s.profile_id)?.name||'Keines')} · Unter Video → Profile freigeben
${e(s.memory_policy)}
Video beendet laufende Deck-GPU-Aufträge und sperrt Chat, Bild und TTS. CPU-STT bleibt verfügbar.
Unter Video → Profile ein Profil freigeben, dann hier den Video-Modus aktivieren und einen kurzen Test starten.
':'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.
Zuerst den Video-Modus aktivieren. Werte überschreiben die Profilstandards für diesen Auftrag. Distilled nutzt seine feste Schrittfolge. Noch kein Bild-/Audio-Upload.
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.
Eigene LTX-Python-Umgebung mit CUDA und Disk-Streaming auf der RTX 5080.
':''}${test?`Kurzer Text-zu-Video-Test mit dem freigegebenen Profil. Werte gelten nur für diesen Auftrag. Noch kein Bild-/Audio-Upload.
Video → Testen bietet einen kurzen Funktionstest; Video → Laufend zeigt den Worker und den aktuellen Auftrag.
'}${e(r.installed?'Installiert':r.job?.phase||'Noch nicht installiert')}
`;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='Falscher Modus oder Video noch nicht bereit. Bitte zuerst in der Übersicht auf Video umschalten.
';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=`${e(x.message)}
`;} + 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=`Status momentan nicht erreichbar: ${e(x.message)}. Ein laufender Auftrag kann weiterlaufen.
`;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}; })(); diff --git a/video.py b/video.py index 2322527..3b44075 100644 --- a/video.py +++ b/video.py @@ -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