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?`
${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=`
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.
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.
${s.reachable?'Daemon erreichbar · Verbindung über begrenzten Systemhelfer':s.helper_available?'Systemhelfer erreichbar · Docker-Daemon nicht erreichbar':'Systemhelfer nicht verbunden'}
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 `
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'),`
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
${e(switching?v.switch_phase:loading?s.worker.phase||'Modell wird geladen':s.state==='stopping'?'Laufende Aufträge werden beendet':'Start wird angefordert')}
`);
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=`
BS-RoFormer und MelBand RoFormer trennen Gesang und Instrumental. Die native Laufzeit wird einmal installiert und für passende Modellpakete wiederverwendet.
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=>`
${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.'}
`).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'