Select the single active video profile through endpoint publication
This commit is contained in:
@@ -157,7 +157,7 @@ Das Rezept unterstützt keine beliebigen Quantisierungen, Comfy-INT8-Encoder ode
|
||||
|
||||
### Exklusiver Video-Modus und Worker
|
||||
|
||||
In der Übersicht das aktive Videoprofil auswählen, dann **Video** drücken. Deck schließt zuerst die GPU-Auftragsannahme, beendet eigene Chat-/Auto-Test-/Bild-/TTS-Arbeit und entlädt seinen llama.cpp-Prozess. Erst nach Freigabe der Reservierungen und Prüfung **aller** sichtbaren GPUs startet der eigene LTX-Worker. Fremde GPU-Prozesse werden niemals beendet; der Wechsel scheitert dann mit einer sichtbaren Meldung. **LLM** beendet den Video-Prozess einschließlich einer laufenden Generierung und öffnet die GPU-Auftragsannahme wieder. Das nächste Chat-Modell lädt bei Anfrage. Nach Deck-Neustart gilt LLM; es gibt keinen automatischen Videostart.
|
||||
Unter **Video → Profile** genau ein Profil mit **Am API-Endpunkt freigegeben** auswählen, dann in der Übersicht **Video** drücken. Ein neuer Haken ersetzt die vorige Video-Freigabe; Abwählen entfernt sie. Die Übersicht zeigt nur den Profilnamen und die Modusschalter. Änderungen der Video-Freigabe erfordern den LLM-Modus. Deck schließt zuerst die GPU-Auftragsannahme, beendet eigene Chat-/Auto-Test-/Bild-/TTS-Arbeit und entlädt seinen llama.cpp-Prozess. Erst nach Freigabe der Reservierungen und Prüfung **aller** sichtbaren GPUs startet der eigene LTX-Worker. Fremde GPU-Prozesse werden niemals beendet; der Wechsel scheitert dann mit einer sichtbaren Meldung. **LLM** beendet den Video-Prozess einschließlich einer laufenden Generierung und öffnet die GPU-Auftragsannahme wieder. Das nächste Chat-Modell lädt bei Anfrage. Nach Deck-Neustart gilt LLM; es gibt keinen automatischen Videostart.
|
||||
|
||||
Die Video-Laufzeit lässt sich unter Einstellungen → Laufzeiten → Video installieren/abbrechen. Sie verwendet eine eigene venv, den in `video_runtime.py` gepinnten offiziellen LTX-Commit sowie `deploy/video-requirements.lock` (PyTorch CUDA 12.8, keine Host-Treiberänderung). Die Module und Lockdatei gehören zu beiden Installationspaketen. Der erste Adapter unterstützt das dokumentierte LTX-2.5-Distilled-BF16-Komponentenrezept. Beide GPUs sind für Deck exklusiv reserviert, die Berechnung erfolgt zunächst auf der RTX 5080 mit Disk-Streaming; die RTX 3060 bleibt frei. „Bereit“ heißt: persistenter Worker, Pipeline und Komponentenmetadaten vorbereitet. BF16-Gewichte werden bedarfsgerecht gestreamt, nicht vollständig im VRAM gehalten. Eine freie GPU garantiert keinen OOM-freien Auftrag.
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ window.EndpointUI=(()=>{
|
||||
root.querySelector('#endpoint-start').disabled=s.state!=='stopped';root.querySelector('#endpoint-stop').disabled=s.state!=='running';
|
||||
if(settings){const port=root.querySelector('#endpoint-port');if(document.activeElement!==port)port.value=s.port;port.disabled=s.state!=='stopped';root.querySelector('#endpoint-port-save').disabled=s.state!=='stopped';
|
||||
root.querySelector('#endpoint-port-help').textContent=s.allowed_ports.length?'Docker-Testinstallation: veröffentlichte Loopback-Ports '+s.allowed_ports.join(', ')+'. Portwechsel nur bei gestopptem Endpunkt. Der SSH-Tunnel muss denselben Port weiterleiten.':'Nativer Dienst: freie Ports zwischen 1024 und 65535, nur bei gestopptem Endpunkt.';
|
||||
put(root.querySelector('#endpoint-profiles'),s.profiles.filter(p=>p.kind!=='video').map(p=>`<div class="note"><strong>${e(p.name)}</strong> · ${e(p.kind)} · ${p.runnable?'ausführbar':e(p.blockers.join(' '))}<br><button class="secondary" data-enable="${e(p.id)}" data-value="${!p.enabled}" ${!p.runnable&&!p.enabled?'disabled':''}>${p.enabled?'Am Endpunkt deaktivieren':'Am Endpunkt aktivieren'}</button></div>`).join('')||'<p>Noch keine Profile angelegt. Erstelle sie in der jeweiligen Modellkategorie.</p>');
|
||||
put(root.querySelector('#endpoint-profiles'),s.profiles.map(p=>`<div class="note"><strong>${e(p.name)}</strong> · ${e(p.kind)} · ${p.runnable?'ausführbar':e(p.blockers.join(' '))}<br><button class="secondary" data-enable="${e(p.id)}" data-value="${!p.enabled}" ${!p.runnable&&!p.enabled?'disabled':''}>${p.enabled?'Am Endpunkt deaktivieren':'Am Endpunkt aktivieren'}</button></div>`).join('')||'<p>Noch keine Profile angelegt. Erstelle sie in der jeweiligen Modellkategorie.</p>');
|
||||
root.querySelectorAll('[data-enable]').forEach(b=>{const p=s.profiles.find(p=>p.id===b.dataset.enable);b.disabled=!p.runnable&&!p.enabled;b.onclick=()=>mutate('/profile',{id:b.dataset.enable,enabled:b.dataset.value==='true'});});
|
||||
}
|
||||
}catch(error){if(root.isConnected)root.querySelector('#endpoint-message').textContent=error.message;}
|
||||
|
||||
+6
-2
@@ -36,7 +36,7 @@ class Endpoint:
|
||||
def rows(self):
|
||||
rows=self.profiles.status()['profiles']
|
||||
with self.lock:enabled=set(self.config['enabled_profiles'])
|
||||
return [dict(p,enabled=p['id'] in enabled) for p in rows]
|
||||
return [dict(p,enabled=(p['id']==self.video.selected if self.video else False) if p['kind']=='video' else p['id'] in enabled) for p in rows]
|
||||
def status(self):
|
||||
rows=self.rows();worker=self.worker.status();job=self.images.status()['job'];counts={}
|
||||
for key,kind in [('llm','chat'),('image','image'),('tts','audio'),('stt','stt')]:
|
||||
@@ -61,8 +61,12 @@ class Endpoint:
|
||||
rows=self.rows()
|
||||
row=next((p for p in rows if p['id']==data['id']),None)
|
||||
if not row:raise ValueError('Profil nicht gefunden.')
|
||||
if row['kind']=='video':raise ValueError('Videoprofil in der Übersicht auswählen; Video nutzt eine exklusive Auswahl.')
|
||||
if data['enabled'] and not row['runnable']:raise ValueError('Profil nicht ausführbar: '+' '.join(row['blockers']))
|
||||
if row['kind']=='video':
|
||||
if not self.video:raise ValueError('Video-Verwaltung nicht eingerichtet.')
|
||||
if data['enabled']:self.video.select(row['id'])
|
||||
elif self.video.selected==row['id']:self.video.select(None)
|
||||
return self.status()
|
||||
with self.lock:
|
||||
enabled=set(self.config['enabled_profiles'])
|
||||
if data['enabled']:
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@ window.ProfilesUI=(()=>{
|
||||
const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},enabled=[],gpus=[],projectors=[],panelSequence=0;
|
||||
const message=t=>{if(root.isConnected)el('profile-message').textContent=t;};
|
||||
async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),kind==='chat'?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length;
|
||||
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}</p>${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'Laufzeit bereit · lädt bei Anfrage'):'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions">${kind==='video'?'<a class="link" href="#home">Als Videoprofil in der Übersicht auswählen →</a>':`<label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label>`}<button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${['image','video'].includes(kind)?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
el('profile-list').insertAdjacentHTML('afterbegin',`<p>${kind==='image'?'Der feste API-Name athena-image verwendet das hier freigegebene Bildprofil und dessen Auflösung. Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':['audio','stt'].includes(kind)?'Eingerichtete Sprachprofile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':kind==='video'?'Ein Videoprofil in der Übersicht auswählen und Video-Modus aktivieren. Aufträge nutzen athena-video; Profilwerte dienen als Standards.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.</p>`);
|
||||
root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message('Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}});
|
||||
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}</p>${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'Laufzeit bereit · lädt bei Anfrage'):'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${['image','video'].includes(kind)?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
el('profile-list').insertAdjacentHTML('afterbegin',`<p>${kind==='image'?'Der feste API-Name athena-image verwendet das hier freigegebene Bildprofil und dessen Auflösung. Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':['audio','stt'].includes(kind)?'Eingerichtete Sprachprofile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':kind==='video'?'Genau ein Videoprofil kann am API-Endpunkt freigegeben sein. Ein neuer Haken ersetzt die bisherige Auswahl. Die Übersicht lädt dieses Profil beim Wechsel auf Video; API-Name: athena-video. Zum Ändern der Freigabe zuerst auf LLM wechseln.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.</p>`);
|
||||
root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message(kind==='video'?'Video-Freigabe gespeichert. Die Übersicht lädt dieses Profil beim Wechsel auf Video.':'Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}});
|
||||
root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}});
|
||||
root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
|
||||
root.querySelectorAll('[data-edit]').forEach(b=>b.onclick=()=>editor(rows.find(p=>p.id===b.dataset.edit)));
|
||||
|
||||
@@ -72,6 +72,19 @@ class EndpointTests(unittest.TestCase):
|
||||
self.assertEqual(self.worker.requests[-1].get('reasoning_effort'),'max' if effort=='ultra' else effort)
|
||||
for effort in ([],True,3,'invalid'):
|
||||
self.assertEqual(self.request('/v1/chat/completions',dict(model='alpha',messages=[dict(role='user',content='synthetic')],reasoning_effort=effort))[0],400)
|
||||
def test_video_publication_is_exclusive_and_can_be_removed(self):
|
||||
for name in ('v1','v2'):
|
||||
self.rows.append(dict(id=name,name=name,kind='video',runnable=True,blockers=[],parameters={},updated_at=1))
|
||||
video=SimpleNamespace(selected=None,status=lambda:{})
|
||||
video.select=lambda ident:setattr(video,'selected',ident)
|
||||
self.ep.video=video
|
||||
self.ep.enable({'id':'v1','enabled':True})
|
||||
self.assertEqual([p['id'] for p in self.ep.rows() if p['kind']=='video' and p['enabled']],['v1'])
|
||||
self.ep.enable({'id':'v2','enabled':True})
|
||||
self.assertEqual([p['id'] for p in self.ep.rows() if p['kind']=='video' and p['enabled']],['v2'])
|
||||
self.ep.enable({'id':'v1','enabled':False});self.assertEqual(video.selected,'v2')
|
||||
self.ep.enable({'id':'v2','enabled':False});self.assertIsNone(video.selected)
|
||||
self.ep.video=None
|
||||
def test_video_mode_rejects_chat_without_starting_llama(self):
|
||||
self.ep.video=Mock();self.ep.scheduler.gpu_mode='video'
|
||||
code,body=self.request('/v1/chat/completions',dict(model='alpha',messages=[dict(role='user',content='synthetic')]))
|
||||
|
||||
@@ -27,6 +27,11 @@ class VideoTests(unittest.TestCase):
|
||||
v=self.make(d);v.select('v');v.blockers=Mock(return_value=[]);v.load=Mock(side_effect=ValueError('busy'))
|
||||
v.switch('video');self.wait(v)
|
||||
self.assertEqual(v.status()['error'],'busy');self.assertEqual(v.scheduler.gpu_mode,'llm')
|
||||
def test_clear_video_selection_persists(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
v=self.make(d);v.select('v');v.select(None)
|
||||
self.assertIsNone(v.selected);self.assertIsNone(self.make(d).selected)
|
||||
with self.assertRaises(ValueError):v.switch('video')
|
||||
def test_foreign_process_prevents_spawn(self):
|
||||
with tempfile.TemporaryDirectory() as d,patch('video.probe',return_value=[{'processes':1}]),patch('video.subprocess.Popen') as spawn:
|
||||
v=self.make(d)
|
||||
|
||||
+3
-4
@@ -2,14 +2,13 @@ window.VideoUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
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 mode(root,s,profiles){
|
||||
if(root.querySelector('#active-video')===document.activeElement||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><label>Aktives Videoprofil<select id="active-video" ${s.mode!=='llm'||s.state==='switching'?'disabled':''}><option value="">Bitte auswählen</option>${profiles.filter(p=>p.kind==='video').map(p=>`<option value="${e(p.id)}" ${s.profile_id===p.id?'selected':''}>${e(p.name)}${p.runnable?'':' · Voraussetzungen fehlen'}</option>`).join('')}</select></label><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>`;
|
||||
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>`;
|
||||
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.querySelector('#active-video').onchange=ev=>{if(ev.target.value)act('video/profile',{id:ev.target.value});};
|
||||
}
|
||||
function html(testing=false){return `<section id="video-runtime-page">${testing?'<h2>Video testen</h2><p>Profil auswählen, 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>`;}
|
||||
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);
|
||||
|
||||
@@ -35,7 +35,7 @@ class Video:
|
||||
with self.lock:
|
||||
if self.mode!='llm' or self.state=='switching':raise ValueError('Videoprofil nur im LLM-Modus wechseln.')
|
||||
p=next((p for p in self.profiles.status()['profiles'] if p['id']==ident and p['kind']=='video'),None)
|
||||
if not p:raise ValueError('Videoprofil nicht gefunden.')
|
||||
if ident is not None and not p:raise ValueError('Videoprofil nicht gefunden.')
|
||||
self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
||||
tmp=self.root/'selection.tmp';tmp.write_text(json.dumps({'profile_id':ident}));tmp.replace(self.root/'selection.json');self.selected=ident
|
||||
return self.status()
|
||||
|
||||
Reference in New Issue
Block a user