Unify separation tools and add exclusive control mode
This commit is contained in:
@@ -238,3 +238,5 @@ Bildadapter werden zentral in `profiles.IMAGE_ADAPTERS` definiert: Modellfamilie
|
||||
### Audio-Trennung
|
||||
|
||||
Eigener Werkzeugbereich `#separation` mit BS-Roformer, MelBand und Demucs FT/6s. Die alte Web-GUI läuft optional als CPU-only Container unter Weitere Dienste und nutzt den nativen Deck-Worker. [Installation und Grenzen](deploy/separator-web/README.md). Modellquellen, alle Ensemble-Dateien und SHA-256 sind in `separator_sources.json` festgelegt. Paket-Downloads und Backup-Restore verwenden diese geprüften Rezepte; Nutzmedien werden nicht exportiert. Bei lokalen Bibliotheksdateien wird eine belegte Originalquelle separat vom Übernahmeort angezeigt, ohne IDs oder Profilreferenzen zu ändern. Unbekannte lokale Quellen werden nicht geraten.
|
||||
|
||||
Audio-Trennung ist auch ein exklusiver Steuerungsmodus neben LLM, Video und Musik. Der Wechsel beendet Deck-eigene GPU-Worker; fremde Dienste bleiben unangetastet. Lange Trennaufträge über die Web-GUI benötigen diesen Modus, der kurze Test bleibt auch im LLM-Modus nutzbar. Sprach-/Bild-/Musik-API-Anfragen erhalten im Trennmodus eine klare 503-Meldung. Der Werkzeugbereich verwendet die gemeinsamen Tabs Entdecken, Bibliothek, Downloads und Testen; Laufzeitinstallation bleibt unter Einstellungen. Weitere Dienste bietet Links und Beschreibung für Separator Web (8128), LadyPoly (8127), LTX DeskWEB (8118) und SwarmUI (8125).
|
||||
|
||||
@@ -16,7 +16,7 @@ docker run -d --name athena-separator-web --network host --restart unless-stoppe
|
||||
-v /opt/athena-separator-web/data:/data:rw athena-separator-web:0.1.0
|
||||
```
|
||||
|
||||
GUI bindet nur 127.0.0.1:8128; Verbindung zu Decks Management-API auf 127.0.0.1:8108. In der GUI einmal Deck-API-Token eintragen. Er wird serverseitig mit Modus 0600 gespeichert und niemals zurückgegeben. Loopback/SSH-Tunnel benutzen.
|
||||
GUI bindet nur 127.0.0.1:8128; Verbindung zu Decks Management-API auf 127.0.0.1:8108. In der Steuerung zuerst Audio-Trennung starten; der Modus beendet Deck-eigene GPU-Worker und sperrt konkurrierende KI-Aufträge. In der GUI einmal Deck-API-Token eintragen. Er wird serverseitig mit Modus 0600 gespeichert und niemals zurückgegeben. Loopback/SSH-Tunnel benutzen.
|
||||
|
||||
Mac-Tunnel: `ssh -i ~/.ssh/athena_key -o BatchMode=yes -N -L 8128:127.0.0.1:8128 root@192.168.1.212`
|
||||
|
||||
|
||||
+8
-1
@@ -1,10 +1,17 @@
|
||||
window.DockerUI=(()=>{
|
||||
const esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const apps={
|
||||
'athena-ladypoly':{title:'LadyPoly YuE2',description:'Weboberfläche für Decks Musiklaufzeit.',port:8127,hint:'In der Steuerung Musik starten. Deck-Verbindung in LadyPoly einrichten.'},
|
||||
'athena-separator-web':{title:'Athena Separator Web',description:'Gesang oder Instrumente herauslösen; Verarbeitung durch Decks native Audio-Separator-Laufzeit.',port:8128,hint:'In der Steuerung Audio-Trennung starten. Modelle unter Audio-Trennung einrichten.'},
|
||||
'ltx-deskweb':{title:'LTX DeskWEB',description:'LTX-Webstudio für Videoerzeugung über Decks originale LTX-Laufzeit.',port:8118,hint:'Videoprofil freigeben und in der Steuerung Video starten.'},
|
||||
'athena-swarm-ui':{title:'SwarmUI',description:'API By URL · Oberfläche für Decks ComfyUI-Laufzeit.',port:8125,hint:'In der Steuerung Video mit ComfyUI starten.'}
|
||||
};
|
||||
function appInfo(c){const a=apps[c.name];return a?`<p>${esc(a.description)}</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':'+a.port+'/')}" target="_blank" rel="noopener">${esc(a.title)} öffnen →</a><p class="small">${esc(a.hint)} SSH-Tunnel: Port ${a.port}.</p>`:'';}
|
||||
async function api(action='',data){const r=await fetch('/api/v1/docker'+action,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});if(r.status===401){location.reload();throw Error('Bitte erneut anmelden.');}const value=await r.json();if(!r.ok)throw Error(value.error||'Docker-Anfrage fehlgeschlagen');return value;}
|
||||
function html(services=false){return `<div class="kicker">${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${services?'Weitere Dienste':'Docker'}</h1><p>${services?'Zusätzliche Oberflächen werden getrennt von Modellen und Laufzeiten verwaltet. Hier erscheinen ausschließlich ausdrücklich für Deck markierte Anwendungscontainer.':'Optionale Umgebung für zusätzliche Studios. Native Modelllaufzeiten bleiben unabhängig davon.'}</p><section id="docker-panel"></section><p id="docker-message" role="status"></p>`;}
|
||||
function bind(services=false){const panel=document.querySelector('#docker-panel'),message=document.querySelector('#docker-message');let signature='',pending=false,timer;
|
||||
async function refresh(){clearTimeout(timer);try{if(pending)return;const s=await api();if(!panel.isConnected)return;const next=JSON.stringify(s);if(signature!==next){signature=next;const running=s.job?.state==='running';
|
||||
if(services){panel.innerHTML=`<section class="card"><h2>Explizite Zuordnung</h2><p>Beide Container-Labels müssen gesetzt sein:</p><pre><code>io.athena-deck.managed=true\nio.athena-deck.role=application</code></pre><p class="small">Keine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Starten und Stoppen betrifft nur markierte Container. Löschen entfernt den Container einschließlich seiner beschreibbaren Schicht; Images und Volumes bleiben erhalten.</p><a class="link" href="#docker-runtime">Docker-Verbindung prüfen →</a></section>${!s.helper_available||!s.reachable?'<section class="empty"><h2>Docker nicht verbunden</h2><p>'+esc(s.message)+'</p></section>':s.services.length?'<div class="grid">'+s.services.map(c=>`<section class="card"><span class="pill">DOCKER · DECK-ANWENDUNG</span><h2>${esc(c.name)}</h2><p>${esc(c.image)}</p><p><strong>${esc(c.state)}</strong> · ${esc(c.status)}</p>${c.name==='athena-ladypoly'?`<p>LadyPoly YuE2 · Oberfläche für Decks Musiklaufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly öffnen →</a><p class="small">Zuerst Deck-Verbindung einrichten und in der Übersicht Musik aktivieren. SSH-Tunnel: Port 8127.</p>`:''}${c.name==='athena-swarm-ui'?`<p>API By URL · Deck steuert die Video-Laufzeit</p><a class="link" href="${esc(location.protocol+'//'+location.hostname+':8125/')}" target="_blank" rel="noopener">SwarmUI öffnen →</a><p class="small">In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.</p>`:''}<div class="card-actions"><button data-container-action="start" data-container-id="${esc(c.id)}" ${c.state==='running'?'disabled':''}>Starten</button><button class="secondary" data-container-action="stop" data-container-id="${esc(c.id)}" ${c.state!=='running'?'disabled':''}>Stoppen</button><button class="secondary danger" data-container-action="delete" data-container-id="${esc(c.id)}" ${!['exited','created','dead'].includes(c.state)?'disabled':''}>Löschen</button></div></section>`).join('')+'</div>' :'<section class="empty"><h2>Noch keine Deck-Anwendungen</h2><p>Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.</p></section>'}`;}
|
||||
if(services){panel.innerHTML=`<section class="card"><h2>Explizite Zuordnung</h2><p>Beide Container-Labels müssen gesetzt sein:</p><pre><code>io.athena-deck.managed=true\nio.athena-deck.role=application</code></pre><p class="small">Keine automatische Übernahme vorhandener Container. Labels werden beim Erstellen in Docker Compose gesetzt. Starten und Stoppen betrifft nur markierte Container. Löschen entfernt den Container einschließlich seiner beschreibbaren Schicht; Images und Volumes bleiben erhalten.</p><a class="link" href="#docker-runtime">Docker-Verbindung prüfen →</a></section>${!s.helper_available||!s.reachable?'<section class="empty"><h2>Docker nicht verbunden</h2><p>'+esc(s.message)+'</p></section>':s.services.length?'<div class="grid">'+s.services.map(c=>`<section class="card"><span class="pill">DOCKER · DECK-ANWENDUNG</span><h2>${esc(c.name)}</h2><p>${esc(c.image)}</p><p><strong>${esc(c.state)}</strong> · ${esc(c.status)}</p>${appInfo(c)}<div class="card-actions"><button data-container-action="start" data-container-id="${esc(c.id)}" ${c.state==='running'?'disabled':''}>Starten</button><button class="secondary" data-container-action="stop" data-container-id="${esc(c.id)}" ${c.state!=='running'?'disabled':''}>Stoppen</button><button class="secondary danger" data-container-action="delete" data-container-id="${esc(c.id)}" ${!['exited','created','dead'].includes(c.state)?'disabled':''}>Löschen</button></div></section>`).join('')+'</div>' :'<section class="empty"><h2>Noch keine Deck-Anwendungen</h2><p>Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.</p></section>'}`;}
|
||||
else {panel.innerHTML=`<section class="card"><span class="pill">${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}</span><h2>Docker Engine ${esc(s.version||'')}</h2><p>${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}</p><p>${esc(s.message)}</p>${s.job?`<p role="status">${esc(s.job.state)} · ${esc(s.job.phase)}</p>`:''}${!s.installed&&s.install_supported&&!running?'<label><input type="checkbox" id="docker-install-ack"> Docker Engine, Compose und Buildx auf diesem Server installieren. Docker richtet einen Dienst und Netzwerk-/Firewallregeln ein.</label><p><button id="docker-install" disabled>Docker installieren</button></p>':`<button disabled>${running?'Installation läuft …':s.installed?'Docker bereits vorhanden':'Installation hier nicht verfügbar'}</button>`}<p class="small">Keine automatische Aktualisierung vorhandener Docker-Installationen. NVIDIA Container Toolkit und Treiber sind separate Voraussetzungen für GPU-Container. Die Paketinstallation wird nicht mitten im Vorgang abgebrochen.</p><a class="link" href="#services">Weitere Dienste öffnen →</a></section>`;
|
||||
const button=panel.querySelector('#docker-install'),ack=panel.querySelector('#docker-install-ack');if(button){ack.onchange=()=>button.disabled=pending||!ack.checked;button.onclick=async()=>{pending=true;button.disabled=true;try{await api('/install',{confirm:true});message.textContent='Docker-Erstinstallation gestartet.';}catch(error){message.textContent=error.message;}finally{pending=false;signature='';}};}
|
||||
}
|
||||
|
||||
+8
-8
@@ -5,20 +5,20 @@ window.EndpointUI=(()=>{
|
||||
async function api(path='',data){const r=await fetch('/api/v1/endpoint'+path,data!==undefined?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Endpunkt nicht erreichbar');return v;}
|
||||
function put(el,html){if(el._deckHtml!==html){el.innerHTML=html;el._deckHtml=html;}}
|
||||
function summary(s){return `<div class="grid"><section class="card wide"><div class="card-top"><h2>OpenAI-kompatibler Endpunkt</h2><span class="pill">${e(stateName[s.state])}</span></div><div class="metrics"><div><small>Erreichbarkeit</small><strong>${s.reachable?'Listener bereit':'Nicht erreichbar'}</strong></div><div><small>Lokaler API-Port</small><strong>${s.port}</strong></div><div><small>Laufende / wartende Aufträge</small><strong>${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}</strong></div></div><p><code>${e(s.base_url)}</code> · <a class="link" href="#endpoint">Port und Profile einstellen →</a></p><p>Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}</p>${s.worker.error||s.error?`<p role="alert">${e(s.worker.error||s.error)}</p>`:''}<p>Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.</p></section>${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `<section class="card"><h3>${label}</h3><p>${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}</p><span class="pill">${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}</span></section>`}).join('')}</div>`;}
|
||||
const modeLabels={llm:'LLM',video:'Video',music:'Musik'};
|
||||
const modeLabels={llm:'LLM',video:'Video',music:'Musik',separator:'Audio-Trennung'};
|
||||
function control(s,root){
|
||||
const v=s.video||{},active=v.mode||'llm';
|
||||
if(!selectedMode)selectedMode=active==='switching'?(v.target_mode||'llm'):active;
|
||||
const selected=selectedMode,label=modeLabels[selected],switching=v.state==='switching',busy=pending||switching||s.state==='stopping';
|
||||
const current=selected===active,available=selected==='video'?Boolean(v.service?.id):Boolean(s.counts[selected]?.available);
|
||||
const ready=current&&s.reachable&&!switching&&(selected!=='video'||v.service?.ready);
|
||||
const current=selected===active,available=selected==='video'?Boolean(v.service?.id):selected==='separator'?Boolean(v.separator?.installed&&v.separator.models?.some(m=>m.installed)):Boolean(s.counts[selected]?.available);
|
||||
const ready=current&&(selected==='separator'||s.reachable)&&!switching&&(selected!=='video'||v.service?.ready);
|
||||
const loading=current&&selected==='llm'&&s.worker.state==='loading';
|
||||
const address=location.protocol+'//'+location.hostname+':'+s.port;
|
||||
const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']];
|
||||
const reason=available?'':selected==='video'?'Wähle zuerst ein eingerichtetes Videomodell.':'Gib zuerst ein ausführbares Profil am API-Endpunkt frei.';
|
||||
const routes=selected==='llm'?[['Chat',address+'/v1/chat/completions'],['Modelle',address+'/v1/models']]:selected==='separator'?[['Trennauftrag',location.protocol+'//'+location.hostname+':8108/api/v1/separator-jobs/start'],['Status',location.protocol+'//'+location.hostname+':8108/api/v1/separator-tests']]:selected==='music'?[['Musik erzeugen',address+'/v1/audio/music'],['Musikmodelle',address+'/v1/audio/music/models']]:[['Video-API',address+'/']];
|
||||
const reason=available?'':selected==='video'?'Wähle zuerst ein eingerichtetes Videomodell.':selected==='separator'?'Installiere zuerst die Laufzeit und ein Trennmodellpaket.':'Gib zuerst ein ausführbares Profil am API-Endpunkt frei.';
|
||||
const state=switching?'Wechsel läuft':loading?'Modell wird geladen':ready?'Bereit':current&&s.state==='stopping'?'Wird gestoppt':current&&s.reachable?'Startet':current?'Gestoppt':'Nicht aktiv';
|
||||
put(root.querySelector('#control-modes'),`<div class="control-modes" role="tablist" aria-label="Betriebsart">${Object.entries(modeLabels).map(([id,name])=>`<button role="tab" aria-selected="${selected===id}" data-control-mode="${id}" class="${selected===id?'chosen':'secondary'}" ${busy?'disabled':''}>${name}</button>`).join('')}</div>`);
|
||||
put(root.querySelector('#endpoint-summary'),`<section class="card control-card" role="tabpanel"><div class="control-heading"><h2>${label}</h2><span class="pill">${e(state)}</span></div><p>${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:s.reachable?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}</p><div class="card-actions"><button data-control-start ${busy||!available||ready?'disabled':''}>${label} starten</button><button data-control-stop class="secondary" ${busy||!current||(s.state==='stopped'&&active==='llm')?'disabled':''}>Stoppen</button></div>${switching||loading||pending||s.state==='stopping'?`<div class="video-activity" role="status" aria-live="polite"><strong>${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}</strong><progress aria-label="Fortschritt"></progress></div>`:''}${reason?`<p class="warning">${e(reason)} <a class="link" href="#${selected==='video'?'video':selected==='music'?'music':'chat'}">Einrichten →</a></p>`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?`<p class="video-error" role="alert">${e(v.error||s.error||s.worker.error)}</p>`:''}<div class="control-api"><h3>API-Adresse</h3>${routes.map(([name,url])=>`<div><span>${name}</span><code>${e(url)}</code></div>`).join('')}<p class="small">${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${s.port}</p></div>${selected==='llm'?`<p class="small">${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}</p>`:selected==='music'?`<p class="small">${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage</p>${available?`<a class="link" href="${e(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly YuE2 öffnen →</a>${!ready?'<p class="small">Vor dem Generieren Musik starten.</p>':''}`:''}`:`<p class="small">${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung</p>${ready?`<a class="link" href="${e(v.runtime_kind==='original'?location.protocol+'//'+location.hostname+':8118/':address+'/')}" target="_blank" rel="noopener">${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →</a>`:''}`}<details class="control-details"><summary>Weitere Dienste & Einstellungen</summary><p>Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.</p><p><a class="link" href="#endpoint">Endpunkt und Port einstellen →</a></p><p>${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge</p></details></section>`);
|
||||
put(root.querySelector('#endpoint-summary'),`<section class="card control-card" role="tabpanel"><div class="control-heading"><h2>${label}</h2><span class="pill">${e(state)}</span></div><p>${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:(s.reachable||active==='separator')?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}</p><div class="card-actions"><button data-control-start ${busy||!available||ready?'disabled':''}>${label} starten</button><button data-control-stop class="secondary" ${busy||!current||(s.state==='stopped'&&active==='llm')?'disabled':''}>Stoppen</button></div>${switching||loading||pending||s.state==='stopping'?`<div class="video-activity" role="status" aria-live="polite"><strong>${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}</strong><progress aria-label="Fortschritt"></progress></div>`:''}${reason?`<p class="warning">${e(reason)} <a class="link" href="#${selected==='video'?'video':selected==='music'?'music':selected==='separator'?'separation':'chat'}">Einrichten →</a></p>`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?`<p class="video-error" role="alert">${e(v.error||s.error||s.worker.error)}</p>`:''}<div class="control-api"><h3>API-Adresse</h3>${routes.map(([name,url])=>`<div><span>${name}</span><code>${e(url)}</code></div>`).join('')}<p class="small">${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${selected==='separator'?8108:s.port}</p></div>${selected==='separator'?`<p class="small">${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.</p><a class="link" href="${e(location.protocol+'//'+location.hostname+':8128/')}" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a><p><a class="link" href="#separation">Modelle und Aufträge →</a></p>`:selected==='llm'?`<p class="small">${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}</p>`:selected==='music'?`<p class="small">${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage</p>${available?`<a class="link" href="${e(location.protocol+'//'+location.hostname+':8127/')}" target="_blank" rel="noopener">LadyPoly YuE2 öffnen →</a>${!ready?'<p class="small">Vor dem Generieren Musik starten.</p>':''}`:''}`:`<p class="small">${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung</p>${ready?`<a class="link" href="${e(v.runtime_kind==='original'?location.protocol+'//'+location.hostname+':8118/':address+'/')}" target="_blank" rel="noopener">${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →</a>`:''}`}<details class="control-details"><summary>Weitere Dienste & Einstellungen</summary><p>Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.</p><p><a class="link" href="#endpoint">Endpunkt und Port einstellen →</a></p><p>${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge</p></details></section>`);
|
||||
root.querySelectorAll('[data-control-mode]').forEach(b=>b.onclick=()=>{selectedMode=b.dataset.controlMode;control(s,root);});
|
||||
root.querySelector('[data-control-start]').onclick=()=>controlAction('start',selected);
|
||||
root.querySelector('[data-control-stop]').onclick=()=>controlAction('stop',selected);
|
||||
@@ -35,9 +35,9 @@ window.EndpointUI=(()=>{
|
||||
const s=await api();
|
||||
if(action==='start'){
|
||||
if((s.video?.mode||'llm')!==mode||(mode==='video'&&!s.video?.service?.ready))await switchMode(mode);
|
||||
if(s.state==='stopped')await api('/start',{});
|
||||
if(mode!=='separator'&&s.state==='stopped')await api('/start',{});
|
||||
}else{
|
||||
await api('/stop',{});
|
||||
if(mode!=='separator')await api('/stop',{});
|
||||
if(s.video&&s.video.mode!=='llm')await switchMode('llm');
|
||||
}
|
||||
if(root.isConnected)root.querySelector('#endpoint-message').textContent='';
|
||||
|
||||
@@ -200,6 +200,7 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
with ep.lock:
|
||||
if not ep.allowed():raise APIError('Endpunkt wird gestoppt.',503,'endpoint_stopping')
|
||||
ep.inflight+=1;admitted=True
|
||||
if ep.scheduler.gpu_mode=='separator':raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
|
||||
if ep.video and ep.scheduler.gpu_mode not in ('llm','music'):
|
||||
if ep.scheduler.gpu_mode=='switching':raise APIError('Moduswechsel läuft.',503,'mode_switching')
|
||||
video=ep.video.status()
|
||||
@@ -212,6 +213,7 @@ class APIHandler(BaseHTTPRequestHandler):
|
||||
if self.command=='GET' and self.path=='/health':return self.send({'status':'ok','service':'athena-deck-api'})
|
||||
if self.video_route(ep):return
|
||||
if self.command!='POST':raise APIError('Route nicht gefunden.',404)
|
||||
if ep.scheduler.gpu_mode=='separator':raise APIError('Audio-Trennung aktiv. Für Chat, Bilder oder Musik die Betriebsart wechseln.',503,'separator_mode_active')
|
||||
if ep.scheduler.gpu_mode=='music' and self.path!='/v1/audio/music':raise APIError('Musik-Modus aktiv. Für andere KI-Aufträge auf LLM wechseln.',503,'music_mode_active')
|
||||
if ep.video and ep.scheduler.gpu_mode not in ('llm','music') and self.path!='/v1/audio/transcriptions':raise APIError('Video-Modus aktiv; Chat-, Bild- und TTS-Aufträge sind gesperrt. Auf LLM zurückschalten.',503,'video_mode_active')
|
||||
if self.path=='/v1/audio/transcriptions':return self.transcription(ep)
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ class Scheduler:
|
||||
self.queue.append(ticket)
|
||||
try:
|
||||
while True:
|
||||
if self.gpu_mode not in modes:raise InferenceError("Video-Modus aktiv oder Moduswechsel läuft; GPU-Aufträge sind gesperrt.")
|
||||
if self.gpu_mode not in modes:raise InferenceError("Andere Betriebsart aktiv oder Moduswechsel läuft; GPU-Auftrag ist gesperrt.")
|
||||
if not allowed():raise InferenceError('Endpunkt wird gestoppt oder Profil ist nicht mehr aktiviert.')
|
||||
if self.queue[0] is ticket and not self.transition and (not self.active or (self.key==key and self.active<slots)) and (not self.tts_active or self.key==key and key!=('image',)):
|
||||
self.queue.pop(0);self.active+=1;claimed=True
|
||||
@@ -60,7 +60,7 @@ class Scheduler:
|
||||
if claimed:
|
||||
with self.cv:self.active-=1;self.cv.notify_all()
|
||||
def image_reservation(self, wait=False,kind='image'):
|
||||
lease=self.lease((kind,),timeout=600 if wait else 0,modes=("music",) if kind=="music" else ("llm",))
|
||||
lease=self.lease((kind,),timeout=600 if wait else 0,modes=("music",) if kind=="music" else ("llm","separator") if kind=="separator" else ("llm",))
|
||||
lease.__enter__()
|
||||
return lambda:lease.__exit__(None,None,None)
|
||||
def tts_reservation(self,wait=False):
|
||||
|
||||
+23
-11
@@ -1,20 +1,32 @@
|
||||
window.SeparatorUI=(()=>{
|
||||
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
let section='discover';
|
||||
async function api(path,data){const response=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const value=await response.json();if(!response.ok)throw Error(value.error||'Anfrage fehlgeschlagen');return value;}
|
||||
function render(test=false){if(document.querySelector('#separator-panel')?.dataset.test===String(test))return;
|
||||
document.querySelector('#view').innerHTML=`<div id="separator-panel" data-test="${test}"><div class="kicker">${test?'KI-WERKZEUGE / AUDIO-TRENNUNG':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${test?'Audio-Trennung':'Audio Separator'}</h1><p>BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.</p>${test?'<p><a href="http://127.0.0.1:8128/" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a> · auf dem Client ist der SSH-Tunnel für Port 8128 erforderlich.</p>':''}<p id="sep-message" role="status"></p><section class="card"><div id="sep-status"></div><button id="sep-install">Laufzeit installieren</button><button class="secondary" id="sep-cancel">Einrichtung abbrechen</button><p>Version 0.47.0 · mindestens 15 GiB frei. Installiert CUDA-PyTorch und Audio Separator in einer eigenen Umgebung. Keine Änderungen an Host-Treibern oder alten Diensten.</p><div id="sep-models"></div><details><summary>Installationsausgabe</summary><pre id="sep-log" style="max-height:240px;overflow:auto;white-space:pre-wrap"></pre></details></section><section class="card"><h2>Kurzer Trenntest</h2><p>PCM-WAV, 16 Bit, Mono oder Stereo, bis 30 Sekunden und 8 MiB. Im LLM-Modus koordiniert Deck die GPU-Nutzung und entlädt gegebenenfalls eigene Modelle. Fremde Dienste bleiben unberührt. Das Trennmodell wird nach dem Test entladen.</p><form id="sep-test"><label>Modellpaket<select name="model" id="sep-model"></select></label><label>Audiodatei<input name="file" type="file" accept=".wav,audio/wav" required></label><button>Audio trennen</button><button type="button" class="secondary" id="sep-test-cancel">Test abbrechen</button></form><div id="sep-job" role="status"></div><div id="sep-results"></div></section></div>`;
|
||||
const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message');let pending=false,signature='';
|
||||
function sources(m){return `<p>Quelle: ${e(m.source?.publisher||'Nicht belegt')}</p><details><summary>Originalquellen und Prüfsummen</summary>${(m.source?.files||[]).map(f=>`<p class="hub-filename">${e(f.file)}<br>${f.urls.map(u=>`<a href="${e(u)}" target="_blank" rel="noopener">Downloadquelle</a>`).join(' · ')}<br><code>${e(f.sha256)}</code></p>`).join('')}</details>`;}
|
||||
function rows(models,library){const list=models.filter(m=>!library||m.installed);return list.length?`<div class="detail-list">${list.map(m=>`<details class="detail-row"><summary class="detail-summary"><strong>${e(m.name)}</strong><span>${m.installed?'Bereit':'Nicht heruntergeladen'}</span><span>${m.source?.files?.length||0} Dateien</span></summary><div class="detail-body"><p class="hub-filename">${e(m.id)}</p>${sources(m)}<p>Vollständiges Modellpaket · einschließlich Konfiguration und benötigter Teilmodelle. Die Laufzeit wird wiederverwendet.</p>${m.installed?'<a class="link" href="#home">In der Steuerung Audio-Trennung starten →</a>':`<button data-sep-model="${e(m.id)}">Modellpaket herunterladen</button>`}</div></details>`).join('')}</div>`:'<section class="empty"><h2>Noch keine Modellpakete</h2><p>Unter Entdecken ein geprüftes Modellpaket herunterladen.</p></section>';}
|
||||
function render(tool=false,force=false){if(!force&&document.querySelector('#separator-panel')?.dataset.tool===String(tool))return;
|
||||
const tabs=[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['test','Testen']];
|
||||
document.querySelector('#view').innerHTML=`<div id="separator-panel" data-tool="${tool}"><div class="kicker">${tool?'ATHENA / MODELLVERWALTUNG':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${tool?'Audio-Trennung':'Audio Separator'}</h1><p>${tool?'Gesang, Schlagzeug, Bass, Gitarre, Klavier oder die übrige Begleitung aus einer Aufnahme herauslösen.':'Native CUDA-Laufzeit für BS-Roformer, MelBand und Demucs. Einmal installieren und für passende Modellpakete wiederverwenden.'}</p>${tool?`<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${tabs.map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-sep-tab="${id}">${label}</button>`).join('')}</div>`:''}<p id="sep-message" role="status"></p><div role="tabpanel"><div id="sep-content"></div></div></div>`;
|
||||
const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message'),content=root.querySelector('#sep-content');let pending=false,signature='';
|
||||
async function action(path,data){pending=true;try{await api(path,data);msg.textContent='Aktion gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}}
|
||||
function bind(s,t){
|
||||
root.querySelectorAll('[data-sep-model]').forEach(b=>{b.disabled=pending||s.job?.state==='running'||!s.installed;b.onclick=()=>action('separator-runtime/download',{model:b.dataset.sepModel});});
|
||||
const install=root.querySelector('#sep-install');if(install){install.disabled=pending||s.installed||s.job?.state==='running';install.onclick=()=>action('separator-runtime/install',{});}
|
||||
const cancel=root.querySelector('#sep-cancel');if(cancel){cancel.disabled=s.job?.state!=='running';cancel.onclick=()=>action('separator-runtime/cancel',{});}
|
||||
const stop=root.querySelector('#sep-test-cancel');if(stop){stop.disabled=t.job?.state!=='running';stop.onclick=()=>action('separator-tests/cancel',{});}
|
||||
const form=root.querySelector('#sep-test');if(form){form.querySelector('button').disabled=pending||t.job?.state==='running'||!s.models.some(m=>m.installed);form.onsubmit=async event=>{event.preventDefault();pending=true;msg.textContent='Audio wird hochgeladen …';try{const response=await fetch('/api/v1/separator-tests/start',{method:'POST',headers:{'X-Athena-Deck':'1'},body:new FormData(form)}),value=await response.json();if(!response.ok)throw Error(value.error);msg.textContent='Trenntest gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}};}
|
||||
}
|
||||
async function poll(){if(!root.isConnected)return;try{const [s,t]=await Promise.all([api('separator-runtime'),api('separator-tests')]);if(!root.isConnected)return;
|
||||
const running=s.job?.state==='running';if(s.job){const log=await api('separator-runtime/log');if(!root.isConnected)return;root.querySelector('#sep-log').textContent=log.text;}root.querySelector('#sep-status').innerHTML=`<p><strong>${s.installed?'Installiert':'Noch nicht installiert'}</strong> · ${e(s.version)}</p>${s.job?`<p>${e(s.job.state)} · ${e(s.job.phase)}</p>${running?'<progress aria-label="Einrichtung läuft"></progress>':''}`:''}`;
|
||||
root.querySelector('#sep-install').disabled=pending||running||s.installed;root.querySelector('#sep-cancel').disabled=pending||!running;
|
||||
const key=JSON.stringify(s.models);if(key!==signature){signature=key;root.querySelector('#sep-models').innerHTML='<h2>Unterstützte Modellpakete</h2><p>Gewichte und passende Konfiguration werden zusammen beschafft. Diese Pakete sind getrennt von ungeprüften Hugging-Face-Dateien.</p>'+s.models.map(m=>`<p><strong>${e(m.name)}</strong> · ${m.installed?'Bereit':'Fehlt'}<br><span class="small">Quelle: ${e(m.source?.publisher||'Nicht belegt')} · geprüftes Komplettpaket</span><details><summary>Originalquellen und Prüfsummen</summary>${(m.source?.files||[]).map(f=>`<p>${e(f.file)}<br>${f.urls.map(u=>`<a href="${e(u)}" target="_blank" rel="noopener">Downloadquelle</a>`).join(" · ")}<br><code>${e(f.sha256)}</code></p>`).join("")}</details><br><span class="small">${e(m.id)} + ${e(m.configuration)}</span><br><button data-sep-model="${e(m.id)}" ${m.installed?'disabled':''}>Modell und Konfiguration herunterladen</button></p>`).join('');root.querySelector('#sep-model').innerHTML=s.models.filter(m=>m.installed).map(m=>`<option value="${e(m.id)}">${e(m.name)}</option>`).join('');root.querySelectorAll('[data-sep-model]').forEach(b=>b.onclick=()=>action('separator-runtime/download',{model:b.dataset.sepModel}));}
|
||||
root.querySelectorAll('[data-sep-model]').forEach(b=>b.disabled=pending||running||!s.installed||s.models.find(m=>m.id===b.dataset.sepModel)?.installed);
|
||||
const j=t.job;root.querySelector('#sep-test button').disabled=pending||running||!s.models.some(m=>m.installed)||j?.state==='running';root.querySelector('#sep-test-cancel').disabled=j?.state!=='running';root.querySelector('#sep-job').innerHTML=j?`<p>${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}</p>${j.state==='running'?'<progress aria-label="Audio wird getrennt"></progress>':''}`:'';
|
||||
const results=root.querySelector('#sep-results');if(results.dataset.job!==String(j?.id+':'+j?.state)){results.dataset.job=String(j?.id+':'+j?.state);results.innerHTML=j?.state==='complete'?j.files.map(f=>`<p>${e(f.name)}</p><audio controls src="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}"></audio><p><a href="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}" download="${e(f.name)}">Spur herunterladen</a></p>`).join(''):'';}
|
||||
const key=JSON.stringify([s,t,pending]);if(key!==signature&& (!root.querySelector('#sep-test')?.contains(document.activeElement)||t.job?.state==='running'||root.dataset.jobState!==String(t.job?.state))){signature=key;root.dataset.jobState=String(t.job?.state);
|
||||
if(!tool){content.innerHTML=`<section class="card"><h2>Laufzeit</h2><p><strong>${s.installed?'Installiert':'Noch nicht installiert'}</strong> · Version ${e(s.version)}</p><button id="sep-install">Laufzeit installieren</button><button class="secondary" id="sep-cancel">Einrichtung abbrechen</button><p>Mindestens 15 GiB frei. Keine Änderung an Host-Treibern oder alten Diensten.</p><a class="link" href="#separation">Audio-Trennung öffnen →</a><p>${e(s.job?.phase||'')}</p>${s.job?.state==='running'?'<progress aria-label="Einrichtung läuft"></progress>':''}<details><summary>Installationsausgabe</summary><pre id="sep-log" style="max-height:240px;overflow:auto;white-space:pre-wrap"></pre></details></section>`;
|
||||
const log=await api('separator-runtime/log');if(root.isConnected)root.querySelector('#sep-log').textContent=log.text;
|
||||
}else if(section==='discover'||section==='library'){content.innerHTML=`${section==='discover'?'<p>Geprüfte Pakete mit allen benötigten Zusatzdateien. Downloads prüfen die Originalquellen per SHA-256.</p>':''}${!s.installed?'<p class="warning">Laufzeit fehlt. <a href="#separator-runtime">Audio Separator installieren →</a></p>':''}${rows(s.models,section==='library')}`;
|
||||
}else if(section==='downloads'){content.innerHTML=s.job?`<section class="card"><span class="pill">${e(s.job.state)}</span><h2>Einrichtungsauftrag</h2><p>${e(s.job.phase)}</p>${s.job.state==='running'?'<progress aria-label="Download läuft"></progress><button id="sep-cancel" class="secondary">Abbrechen</button>':''}<p>Abgeschlossene Modellpakete findest du in der Bibliothek.</p></section>`:'<section class="empty"><h2>Keine Downloads</h2><p>Unter Entdecken ein Modellpaket auswählen.</p></section>';
|
||||
}else{const j=t.job;content.innerHTML=`<section class="card"><h2>Audio-Trennung testen</h2><p>Kurzer PCM-WAV-Test: 16 Bit, Mono/Stereo, bis 30 Sekunden und 8 MiB. Für längere Dateien die Weboberfläche verwenden.</p><p><a class="link" href="${e(location.protocol+'//'+location.hostname+':8128/')}" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a> · <a href="#home">Audio-Trennung in der Steuerung starten →</a></p><p class="small">Gewichte laden bei Auftrag und werden anschließend entladen. Fremde Dienste bleiben unberührt.</p><form id="sep-test"><label>Modellpaket<select name="model">${s.models.filter(m=>m.installed).map(m=>`<option value="${e(m.id)}">${e(m.name)}</option>`).join('')}</select></label><label>Audiodatei<input name="file" type="file" accept=".wav,audio/wav" required></label><button>Audio trennen</button><button type="button" class="secondary" id="sep-test-cancel">Abbrechen</button></form><div role="status">${j?`<p>${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}</p>${j.state==='running'?'<progress aria-label="Audio wird getrennt"></progress>':''}`:''}</div>${j?.state==='complete'?j.files.map(f=>`<p>${e(f.name)}</p><audio controls src="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}"></audio><p><a href="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}" download="${e(f.name)}">Spur herunterladen</a></p>`).join(''):''}</section>`;}
|
||||
}
|
||||
bind(s,t);
|
||||
}catch(error){if(root.isConnected)msg.textContent=error.message;}clearTimeout(root.pollTimer);if(root.isConnected)root.pollTimer=setTimeout(poll,3000);}
|
||||
root.querySelector('#sep-install').onclick=()=>action('separator-runtime/install',{});root.querySelector('#sep-cancel').onclick=()=>action('separator-runtime/cancel',{});root.querySelector('#sep-test-cancel').onclick=()=>action('separator-tests/cancel',{});
|
||||
root.querySelector('#sep-test').onsubmit=async event=>{event.preventDefault();pending=true;msg.textContent='Audio wird hochgeladen …';try{const response=await fetch('/api/v1/separator-tests/start',{method:'POST',headers:{'X-Athena-Deck':'1'},body:new FormData(event.target)}),value=await response.json();if(!response.ok)throw Error(value.error);msg.textContent='Trenntest gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;poll();}};poll();
|
||||
root.querySelectorAll('[data-sep-tab]').forEach(b=>b.onclick=()=>{section=b.dataset.sepTab;render(tool,true);});poll();
|
||||
}
|
||||
return {render};
|
||||
})();
|
||||
|
||||
@@ -9,6 +9,7 @@ class SeparatorTests:
|
||||
def status(self):
|
||||
with self.lock:return dict(job=dict(self.job) if self.job else None)
|
||||
def start(self,model,audio,target=None,full=False):
|
||||
if full and self.scheduler.gpu_mode!='separator':raise ValueError('Bitte unter Steuerung Audio-Trennung starten.')
|
||||
targets={'vocals':'model_bs_roformer_ep_317_sdr_12.9755.ckpt','drums':'htdemucs_ft.yaml','bass':'htdemucs_ft.yaml','guitar':'htdemucs_6s.yaml','piano':'htdemucs_6s.yaml','other':'htdemucs_6s.yaml'}
|
||||
if target is not None and targets.get(target)!=model:raise ValueError('Zielspur und Modellpaket passen nicht zusammen.')
|
||||
if not isinstance(model,str) or model not in MODELS or not self.runtime.status()['installed'] or not self.runtime.model_ready(model):raise ValueError('Laufzeit und Modellpaket zuerst unter Audio Separator einrichten.')
|
||||
|
||||
@@ -131,6 +131,7 @@ class Server(ThreadingHTTPServer):
|
||||
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.video.separator_status=lambda:dict(self.separator_runtime.status(),worker=self.separator_tests.status())
|
||||
self.video.music_profiles=lambda:[dict(id=p['id'],name=p['name']) for p in self.endpoint.rows() if p['kind']=='music' and p['enabled'] and p['runnable']]
|
||||
self.profiles.video_runtime_ready=lambda:self.image_runtime.status().get("installed",False)
|
||||
from dashboard_data import Dashboard
|
||||
@@ -573,7 +574,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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, video oder music erwartet.')
|
||||
if set(data)!={'mode'}:raise ValueError('Nur Modus llm, video, music oder separator erwartet.')
|
||||
return self.respond(self.server.video.switch(data['mode']))
|
||||
except (OSError,ValueError) as exc:return self.respond({'error':str(exc)},400)
|
||||
if self.path in ('/api/v1/endpoint/start','/api/v1/endpoint/stop','/api/v1/endpoint/config','/api/v1/endpoint/profile'):
|
||||
|
||||
@@ -90,6 +90,12 @@ class EndpointTests(unittest.TestCase):
|
||||
response=conn.getresponse();self.assertEqual(response.status,200);self.assertEqual(response.getheader('Content-Type'),'audio/wav');self.assertEqual(response.read(),b'RIFFsynthetic-WAV');conn.close()
|
||||
self.assertEqual(self.request('/v1/audio/music',dict(model='Music',lyrics='Synthetic',style='pop',unsupported=True))[0],400)
|
||||
self.assertEqual(self.request('/v1/audio/music/models',token='bad')[0],401)
|
||||
def test_separator_mode_rejects_chat_without_starting_worker_or_relay(self):
|
||||
self.ep.scheduler.gpu_mode='separator';self.ep.video=Mock()
|
||||
status,error=self.request('/v1/chat/completions',dict(model='alpha',messages=[dict(role='user',content='Synthetic')]))
|
||||
self.assertEqual(status,503);self.assertEqual(error['error']['code'],'separator_mode_active')
|
||||
self.assertEqual(self.worker.started,[]);self.ep.video.relay.assert_not_called()
|
||||
self.assertEqual(self.request('/v1/models')[0],503)
|
||||
def enable(self,name):self.ep.enable(dict(id=name,enabled=True))
|
||||
def test_explicit_publication_auth_rotation_and_missing_audio(self):
|
||||
self.assertEqual(self.request()[1]['data'],[]);self.assertEqual(self.request(token='bad')[0],401)
|
||||
|
||||
@@ -54,4 +54,11 @@ class Tests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
worker=SeparatorTests(root,Mock(),Mock());directory=Path(root,'a'*32);directory.mkdir();(directory/'complete').touch();(directory/'results.json').write_text('["../../secret.wav"]')
|
||||
with self.assertRaises(ValueError):worker.audio('a'*32,0)
|
||||
|
||||
def test_full_job_requires_separator_mode_before_thread_starts(self):
|
||||
with tempfile.TemporaryDirectory() as root:
|
||||
scheduler=Mock();scheduler.gpu_mode='llm'
|
||||
worker=SeparatorTests(root,Mock(),scheduler)
|
||||
with self.assertRaisesRegex(ValueError,'Steuerung'):worker.start(next(iter(MODELS)),audio(),full=True)
|
||||
|
||||
if __name__=='__main__':unittest.main()
|
||||
|
||||
@@ -56,3 +56,16 @@ class VideoComfyTests(unittest.TestCase):
|
||||
self.video.switch('llm');self.video.thread.join(2);self.assertEqual(scheduler.gpu_mode,'llm')
|
||||
def test_music_requires_enabled_ready_profile(self):
|
||||
with self.assertRaisesRegex(ValueError,'Musikprofil'):self.video.switch('music')
|
||||
|
||||
def test_separator_switch_drains_owned_workers_without_starting_video(self):
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
self.video.scheduler=SimpleNamespace(gpu_mode='llm',cv=threading.Condition(),active=0,tts_active=0,transition=False,key=('chat',))
|
||||
self.video._stop=Mock();self.video._start=Mock();self.video._foreign_services=lambda:[]
|
||||
self.video.separator_status=lambda:dict(installed=True,models=[dict(installed=True)])
|
||||
self.video.switch('separator');self.video.thread.join(2)
|
||||
self.assertEqual(self.video.scheduler.gpu_mode,'separator');self.video._start.assert_not_called();self.video.stop_owned.assert_called_once()
|
||||
self.video.switch('llm');self.video.thread.join(2);self.assertEqual(self.video.scheduler.gpu_mode,'llm')
|
||||
def test_separator_requires_ready_model(self):
|
||||
self.video.separator_status=lambda:dict(installed=False,models=[])
|
||||
with self.assertRaisesRegex(ValueError,'Audio-Separator'):self.video.switch('separator')
|
||||
|
||||
+6
-4
@@ -55,12 +55,13 @@ class VideoComfy:
|
||||
self.state='failed';self.error='ComfyUI wurde unerwartet beendet. Auf LLM wechseln und Laufzeit erneut starten.'
|
||||
rows=self.models();ident=self.selection.get('model_id');row=next((x for x in rows if x['model']['id']==ident),None)
|
||||
service=dict(id=ident,name=row['model']['file'] if row else 'Kein Videomodell ausgewählt',running=running,ready=running and self.state=='ready',api_url='/ (ComfyUI auf dem gemeinsamen API-Port)',message='5080: Diffusion/VAE · 3060: Textencoder · große BF16-Gewichte nutzen CPU-Auslagerung.') if row else {}
|
||||
return dict(mode=self.scheduler.gpu_mode,state=self.state,error=self.error,target_mode=self.target_mode,switch_phase=self.switch_phase,switch_started_at=self.switch_started_at,service=service,music_profiles=getattr(self,'music_profiles',lambda:[])(),services=[dict(id=x['model']['id'],name=x['model']['file'],api_url=x['label'],**{k:x[k] for k in ('blockers','runnable')}) for x in rows],models=rows,selected=ident,memory_policy='Gemeinsame ComfyUI-Installation. Video startet einen eigenen Prozess; Gewichte laden bei Workflow-Anfrage. Der Wechsel auf LLM beendet den Prozess.',runtime=self.runtime.status())
|
||||
return dict(mode=self.scheduler.gpu_mode,state=self.state,error=self.error,target_mode=self.target_mode,switch_phase=self.switch_phase,switch_started_at=self.switch_started_at,service=service,music_profiles=getattr(self,'music_profiles',lambda:[])(),separator=getattr(self,'separator_status',lambda:{})(),services=[dict(id=x['model']['id'],name=x['model']['file'],api_url=x['label'],**{k:x[k] for k in ('blockers','runnable')}) for x in rows],models=rows,selected=ident,memory_policy='Gemeinsame ComfyUI-Installation. Video startet einen eigenen Prozess; Gewichte laden bei Workflow-Anfrage. Der Wechsel auf LLM beendet den Prozess.',runtime=self.runtime.status())
|
||||
def switch(self,mode):
|
||||
if mode not in ('llm','video','music'):raise ValueError('Modus muss llm, video oder music sein.')
|
||||
if mode not in ('llm','video','music','separator'):raise ValueError('Modus muss llm, video, music oder separator sein.')
|
||||
with self.lock:
|
||||
if self.state=='switching':raise ValueError('Moduswechsel läuft bereits.')
|
||||
if mode=='music' and not getattr(self,'music_profiles',lambda:[])():raise ValueError('Zuerst ein ausführbares Musikprofil am API-Endpunkt freigeben.')
|
||||
if mode=='separator' and not any(m['installed'] for m in getattr(self,'separator_status',lambda:{'models':[]})().get('models',[])):raise ValueError('Zuerst Audio-Separator-Laufzeit und ein Modellpaket einrichten.')
|
||||
if mode=='video':
|
||||
row=next((x for x in self.models() if x['model']['id']==self.selection.get('model_id')),None)
|
||||
if not row:raise ValueError('Zuerst Videomodell aktivieren.')
|
||||
@@ -77,7 +78,7 @@ class VideoComfy:
|
||||
if time.monotonic()>deadline:raise ValueError('Deck-Aufträge noch aktiv.')
|
||||
self.scheduler.cv.wait(.2)
|
||||
self.scheduler.key=None
|
||||
if mode in ('llm','music'):
|
||||
if mode in ('llm','music','separator'):
|
||||
if any(x.get('running') is not False for x in self._foreign_services()):raise ValueError('Fremder Videodienst blockiert LLM-Modus.')
|
||||
self.foreign_blocked=False
|
||||
else:
|
||||
@@ -92,7 +93,8 @@ class VideoComfy:
|
||||
self.scheduler.key=None
|
||||
self.switch_phase='Freie GPUs prüfen';self._start()
|
||||
with self.scheduler.cv:self.scheduler.gpu_mode=mode;self.scheduler.cv.notify_all()
|
||||
self.state='ready' if mode in ('video','music') else 'idle'
|
||||
self.state='ready' if mode in ('video','music','separator') else 'idle'
|
||||
if mode=='separator':self.switch_phase='Audio-Trennung bereit · Gewichte laden bei Auftrag'
|
||||
if mode=='music':self.switch_phase='Musik bereit · Gewichte laden bei Anfrage'
|
||||
except Exception as exc:
|
||||
self._stop();self.error=str(exc);self.state='failed'
|
||||
|
||||
Reference in New Issue
Block a user