diff --git a/README.md b/README.md index b0eb004..42ca9a8 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/deploy/separator-web/README.md b/deploy/separator-web/README.md index 8248639..f2be3fd 100644 --- a/deploy/separator-web/README.md +++ b/deploy/separator-web/README.md @@ -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` diff --git a/docker-ui.js b/docker-ui.js index 4771471..d298bad 100644 --- a/docker-ui.js +++ b/docker-ui.js @@ -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?`

${esc(a.description)}

${esc(a.title)} öffnen →

${esc(a.hint)} SSH-Tunnel: Port ${a.port}.

`:'';} 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 `
${services?'ANWENDUNGEN':'EINSTELLUNGEN / LAUFZEITEN'}

${services?'Weitere Dienste':'Docker'}

${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.'}

`;} 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=`

Explizite Zuordnung

Beide Container-Labels müssen gesetzt sein:

io.athena-deck.managed=true\nio.athena-deck.role=application

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.

Docker-Verbindung prüfen →
${!s.helper_available||!s.reachable?'

Docker nicht verbunden

'+esc(s.message)+'

':s.services.length?'
'+s.services.map(c=>`
DOCKER · DECK-ANWENDUNG

${esc(c.name)}

${esc(c.image)}

${esc(c.state)} · ${esc(c.status)}

${c.name==='athena-ladypoly'?`

LadyPoly YuE2 · Oberfläche für Decks Musiklaufzeit

LadyPoly öffnen →

Zuerst Deck-Verbindung einrichten und in der Übersicht Musik aktivieren. SSH-Tunnel: Port 8127.

`:''}${c.name==='athena-swarm-ui'?`

API By URL · Deck steuert die Video-Laufzeit

SwarmUI öffnen →

In Deck zuerst Video aktivieren. Zugriff über den SSH-Tunnel auf Port 8125.

`:''}
`).join('')+'
' :'

Noch keine Deck-Anwendungen

Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.

'}`;} + if(services){panel.innerHTML=`

Explizite Zuordnung

Beide Container-Labels müssen gesetzt sein:

io.athena-deck.managed=true\nio.athena-deck.role=application

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.

Docker-Verbindung prüfen →
${!s.helper_available||!s.reachable?'

Docker nicht verbunden

'+esc(s.message)+'

':s.services.length?'
'+s.services.map(c=>`
DOCKER · DECK-ANWENDUNG

${esc(c.name)}

${esc(c.image)}

${esc(c.state)} · ${esc(c.status)}

${appInfo(c)}
`).join('')+'
' :'

Noch keine Deck-Anwendungen

Docker ist erreichbar. Kein Container trägt beide erforderlichen Labels. Bestehende Router-, WireGuard- und andere Container bleiben ausgeblendet.

'}`;} else {panel.innerHTML=`
${s.installed?'INSTALLIERT':s.installed===false?'NICHT INSTALLIERT':'NICHT ERMITTELBAR'}

Docker Engine ${esc(s.version||'')}

${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}

${esc(s.message)}

${s.job?`

${esc(s.job.state)} · ${esc(s.job.phase)}

`:''}${!s.installed&&s.install_supported&&!running?'

':``}

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.

Weitere Dienste öffnen →
`; 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='';}};} } diff --git a/endpoint-ui.js b/endpoint-ui.js index c27d63d..07f30fb 100644 --- a/endpoint-ui.js +++ b/endpoint-ui.js @@ -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 `

OpenAI-kompatibler Endpunkt

${e(stateName[s.state])}
Erreichbarkeit${s.reachable?'Listener bereit':'Nicht erreichbar'}
Lokaler API-Port${s.port}
Laufende / wartende Aufträge${s.scheduler.active_requests} / ${s.scheduler.waiting_requests}

${e(s.base_url)} · Port und Profile einstellen →

Geladen: ${e(s.worker.profile_name||'Kein Sprachmodell')} · ${e(stateName[s.worker.state]||s.worker.state)}

${s.worker.error||s.error?`

${e(s.worker.error||s.error)}

`:''}

Aktivierte Profile werden angeboten. Erst eine Anfrage lädt das Modell. Wechsel warten auf laufende Antworten; Bildaufträge entladen vorher das eigene Sprachmodell.

${[['llm','Sprachmodelle'],['image','Bildgenerierung'],['tts','Text → Sprache (TTS)'],['stt','Sprache → Text (STT)'],['music','Musik']].map(([key,label])=>{const c=s.counts[key];return `

${label}

${c.supported?`${c.enabled} Profile aktiviert · ${c.available} ausführbar`:'Noch keine eigene Laufzeit eingerichtet'}

${c.loaded?'AKTIV IM SPEICHER':c.available?'BEI ANFRAGE LADEN':'NICHT AKTIV'}
`}).join('')}
`;} - 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'),`
${Object.entries(modeLabels).map(([id,name])=>``).join('')}
`); - put(root.querySelector('#endpoint-summary'),`

${label}

${e(state)}

${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:s.reachable?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}

${switching||loading||pending||s.state==='stopping'?`
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
`:''}${reason?`

${e(reason)} Einrichten →

`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?``:''}

API-Adresse

${routes.map(([name,url])=>`
${name}${e(url)}
`).join('')}

${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${s.port}

${selected==='llm'?`

${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}

`:selected==='music'?`

${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage

${available?`LadyPoly YuE2 öffnen →${!ready?'

Vor dem Generieren Musik starten.

':''}`:''}`:`

${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung

${ready?`${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →`:''}`}
Weitere Dienste & Einstellungen

Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.

Endpunkt und Port einstellen →

${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge

`); + put(root.querySelector('#endpoint-summary'),`

${label}

${e(state)}

${active==='switching'?`Wechsel zu ${e(modeLabels[v.target_mode]||v.target_mode)}`:(s.reachable||active==='separator')?`Aktiver Betrieb: ${e(modeLabels[active]||active)}`:'Betrieb gestoppt'}

${switching||loading||pending||s.state==='stopping'?`
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
`:''}${reason?`

${e(reason)} Einrichten →

`:''}${(current&&(v.error||s.error||selected==='llm'&&s.worker.error))?``:''}

API-Adresse

${routes.map(([name,url])=>`
${name}${e(url)}
`).join('')}

${ready?'Erreichbar':'Erreichbar, sobald dieser Betrieb bereit ist'} · Port ${selected==='separator'?8108:s.port}

${selected==='separator'?`

${v.separator?.models?.filter(m=>m.installed).length||0} Modellpakete bereit · Gewichte laden beim Trennauftrag und werden anschließend entladen.

Audio-Trennoberfläche öffnen →

Modelle und Aufträge →

`:selected==='llm'?`

${s.counts.llm.available} Chatprofile freigegeben · ${e(s.worker.profile_name||'Gewichte laden bei der ersten Anfrage')}

`:selected==='music'?`

${e(v.music_profiles?.map(p=>p.name).join(', ')||'Kein Musikprofil freigegeben')} · Gewichte laden bei Anfrage

${available?`LadyPoly YuE2 öffnen →${!ready?'

Vor dem Generieren Musik starten.

':''}`:''}`:`

${e(v.service?.name||'Kein Videomodell ausgewählt')} · ${v.runtime_kind==='original'?'LTX Original':'ComfyUI'} · Gewichte laden bei Generierung

${ready?`${v.runtime_kind==='original'?'LTX DeskWEB':'ComfyUI'} öffnen →`:''}`}
Weitere Dienste & Einstellungen

Im LLM-Betrieb stehen auch freigegebene Bild-, TTS- und STT-Profile bereit.

Endpunkt und Port einstellen →

${s.scheduler.active_requests} laufende · ${s.scheduler.waiting_requests} wartende Aufträge

`); 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=''; diff --git a/endpoint.py b/endpoint.py index eea79d6..4d3e011 100644 --- a/endpoint.py +++ b/endpoint.py @@ -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) diff --git a/inference.py b/inference.py index c3649ef..3b509a7 100644 --- a/inference.py +++ b/inference.py @@ -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{ 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=`
${test?'KI-WERKZEUGE / AUDIO-TRENNUNG':'EINSTELLUNGEN / LAUFZEITEN'}

${test?'Audio-Trennung':'Audio Separator'}

BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.

${test?'

Audio-Trennoberfläche öffnen → · auf dem Client ist der SSH-Tunnel für Port 8128 erforderlich.

':''}

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.

Installationsausgabe

Kurzer Trenntest

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.

`; - const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message');let pending=false,signature=''; + function sources(m){return `

Quelle: ${e(m.source?.publisher||'Nicht belegt')}

Originalquellen und Prüfsummen${(m.source?.files||[]).map(f=>`

${e(f.file)}
${f.urls.map(u=>`Downloadquelle`).join(' · ')}
${e(f.sha256)}

`).join('')}
`;} + function rows(models,library){const list=models.filter(m=>!library||m.installed);return list.length?`
${list.map(m=>`
${e(m.name)}${m.installed?'Bereit':'Nicht heruntergeladen'}${m.source?.files?.length||0} Dateien

${e(m.id)}

${sources(m)}

Vollständiges Modellpaket · einschließlich Konfiguration und benötigter Teilmodelle. Die Laufzeit wird wiederverwendet.

${m.installed?'In der Steuerung Audio-Trennung starten →':``}
`).join('')}
`:'

Noch keine Modellpakete

Unter Entdecken ein geprüftes Modellpaket herunterladen.

';} + 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=`
${tool?'ATHENA / MODELLVERWALTUNG':'EINSTELLUNGEN / LAUFZEITEN'}

${tool?'Audio-Trennung':'Audio Separator'}

${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.'}

${tool?`
${tabs.map(([id,label])=>``).join('')}
`:''}

`; + 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=`

${s.installed?'Installiert':'Noch nicht installiert'} · ${e(s.version)}

${s.job?`

${e(s.job.state)} · ${e(s.job.phase)}

${running?'':''}`:''}`; - 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='

Unterstützte Modellpakete

Gewichte und passende Konfiguration werden zusammen beschafft. Diese Pakete sind getrennt von ungeprüften Hugging-Face-Dateien.

'+s.models.map(m=>`

${e(m.name)} · ${m.installed?'Bereit':'Fehlt'}
Quelle: ${e(m.source?.publisher||'Nicht belegt')} · geprüftes Komplettpaket

Originalquellen und Prüfsummen${(m.source?.files||[]).map(f=>`

${e(f.file)}
${f.urls.map(u=>`Downloadquelle`).join(" · ")}
${e(f.sha256)}

`).join("")}

${e(m.id)} + ${e(m.configuration)}

`).join('');root.querySelector('#sep-model').innerHTML=s.models.filter(m=>m.installed).map(m=>``).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?`

${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}

${j.state==='running'?'':''}`:''; - 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=>`

${e(f.name)}

Spur herunterladen

`).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=`

Laufzeit

${s.installed?'Installiert':'Noch nicht installiert'} · Version ${e(s.version)}

Mindestens 15 GiB frei. Keine Änderung an Host-Treibern oder alten Diensten.

Audio-Trennung öffnen →

${e(s.job?.phase||'')}

${s.job?.state==='running'?'':''}
Installationsausgabe
`; + 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'?'

Geprüfte Pakete mit allen benötigten Zusatzdateien. Downloads prüfen die Originalquellen per SHA-256.

':''}${!s.installed?'

Laufzeit fehlt. Audio Separator installieren →

':''}${rows(s.models,section==='library')}`; + }else if(section==='downloads'){content.innerHTML=s.job?`
${e(s.job.state)}

Einrichtungsauftrag

${e(s.job.phase)}

${s.job.state==='running'?'':''}

Abgeschlossene Modellpakete findest du in der Bibliothek.

`:'

Keine Downloads

Unter Entdecken ein Modellpaket auswählen.

'; + }else{const j=t.job;content.innerHTML=`

Audio-Trennung testen

Kurzer PCM-WAV-Test: 16 Bit, Mono/Stereo, bis 30 Sekunden und 8 MiB. Für längere Dateien die Weboberfläche verwenden.

Audio-Trennoberfläche öffnen → · Audio-Trennung in der Steuerung starten →

Gewichte laden bei Auftrag und werden anschließend entladen. Fremde Dienste bleiben unberührt.

${j?`

${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}

${j.state==='running'?'':''}`:''}
${j?.state==='complete'?j.files.map(f=>`

${e(f.name)}

Spur herunterladen

`).join(''):''}
`;} + } + 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}; })(); diff --git a/separator.py b/separator.py index 694afce..5b46adb 100644 --- a/separator.py +++ b/separator.py @@ -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.') diff --git a/server.py b/server.py index d5cb95f..c1daec3 100644 --- a/server.py +++ b/server.py @@ -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'): diff --git a/test_endpoint.py b/test_endpoint.py index 8b0ab0b..14f1233 100644 --- a/test_endpoint.py +++ b/test_endpoint.py @@ -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) diff --git a/test_separator.py b/test_separator.py index 8b413e4..b15b6bd 100644 --- a/test_separator.py +++ b/test_separator.py @@ -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() diff --git a/test_video_comfy.py b/test_video_comfy.py index 8a545bb..8705bb9 100644 --- a/test_video_comfy.py +++ b/test_video_comfy.py @@ -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') diff --git a/video_comfy.py b/video_comfy.py index c5763ba..cfc9c15 100644 --- a/video_comfy.py +++ b/video_comfy.py @@ -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'