Add separation profiles with activation and explicit GPU assignment

This commit is contained in:
Mikei386 committed 2026-10-01 20:43:56 +02:00
1 parent 356abbeb9d
commit 932284a831
8 files changed
+82 -8

No files matched your search

+8
View File
@@ -240,3 +240,11 @@ Bildadapter werden zentral in `profiles.IMAGE_ADAPTERS` definiert: Modellfamilie
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).
Unter **Audio-Trennung → Profile** gibt es ein aufklappbares Profil je geprüftem
Modellpaket: Aktivierung für Trennaufträge und Auswahl einer festen GPU oder
„Automatisch · RTX 5080 zuerst“. Automatik nimmt anschließend die RTX 3060, wenn
die 5080 nicht frei genug ist. Eine feste Auswahl fällt nicht still auf eine
andere Karte zurück. Auch die Separator-Weboberfläche beachtet diese Einstellungen.
Profileinstellungen werden im verschlüsselten Backup gespeichert. Die GPU-Prüfung
ist eine Vorprüfung; ein tatsächlicher OOM wird weiterhin als Auftragsfehler angezeigt.
+4 -1
View File
@@ -7,7 +7,7 @@ from auth import atomic_write,validate_record
from backup_codec import seal,open_backup
from catalog import repo_id,file_role,KINDS
from profiles import SCHEMAS,CHAT_GPU_DEFAULTS,chat_parameters
SETTINGS=('profiles.json','endpoint.json','audio-policy.json','video/comfy-selection.json','models/huggingface.json','video/comfy-client-token','video/original-work/settings.json')
SETTINGS=('separator-profiles.json','profiles.json','endpoint.json','audio-policy.json','video/comfy-selection.json','models/huggingface.json','video/comfy-client-token','video/original-work/settings.json')
MAX_HISTORY=40*1024**2
VERSION=1
# Exact byte hash verified against official Hub metadata, not a filename heuristic.
@@ -70,6 +70,9 @@ def validate(doc):
if not isinstance(hf,dict) or set(hf)!={'token'} or hf['token'] is not None and (not isinstance(hf['token'],str) or not re.fullmatch(r'hf_[A-Za-z0-9]{10,252}',hf['token'])):raise ValueError('Ungültiger Hugging-Face-Zugang.')
if 'video/comfy-client-token' in settings and not re.fullmatch(r'[A-Za-z0-9_-]{32,256}',settings['video/comfy-client-token'].strip()):raise ValueError('Ungültiger Diensttoken.')
if 'video/original-work/settings.json' in settings and (not isinstance(settings['video/original-work/settings.json'],dict) or len(json.dumps(settings['video/original-work/settings.json']))>65536):raise ValueError('Ungültige LTX-Einstellungen.')
if 'separator-profiles.json' in settings:
from separator import validate_profiles
validate_profiles(settings['separator-profiles.json'])
validate_record(doc.get('credentials'))
runtimes=doc.get('runtimes')
if not isinstance(runtimes,dict) or set(runtimes)-{'llama','llama_builds','image','tts','enhancers','swarm_nodes','ltx_original','audio_cpp','separator'} or not {'llama','llama_builds','image','tts','enhancers','swarm_nodes'}<=set(runtimes):raise ValueError('Ungültiges Laufzeitrezept.')
+6
View File
@@ -66,3 +66,9 @@ Korrigiert: falsche Warnung über fehlende Quellen bei lokalen Build-Rezepten;
Separator-Web erhält beim Restore wieder eine schreibgeschützte Rootfs und seine
beschreibbare 1-GiB-Tmpfs. Änderungen benötigen zur Live-Aktivierung das nächste
Deck-App-Update und die Aktualisierung des Docker-Systemhelfers.
Audio-Trennprofile werden zusätzlich in `separator-profiles.json` gesichert:
Aktivierung und GPU-UUID je geprüftem Modellpaket. Alte Backups ohne diese Datei
verwenden aktivierte Profile mit automatischer Auswahl (RTX 5080 zuerst). Bei
geänderter Hardware muss eine fest gespeicherte UUID im Profil neu gewählt werden;
Deck wechselt bei fester Auswahl nicht unbemerkt auf eine andere GPU.
+6 -4
View File
@@ -5,11 +5,12 @@ window.SeparatorUI=(()=>{
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']];
const tabs=[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['profiles','Profile'],['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();}}
async function action(path,data){pending=true;try{await api(path,data);msg.textContent=path==='separator-profiles/save'?'Profil gespeichert.':'Aktion gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}}
function bind(s,t){
root.querySelectorAll('[data-sep-profile]').forEach(form=>{form.onsubmit=event=>{event.preventDefault();action('separator-profiles/save',{model:form.dataset.sepProfile,enabled:form.elements.enabled.checked,device:form.elements.device.value});};});
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',{});}
@@ -17,12 +18,13 @@ window.SeparatorUI=(()=>{
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 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);
const key=JSON.stringify([s,t,pending]);if(key!==signature&& (!root.querySelector('form')?.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==='profiles'){content.innerHTML=`${t.hardware_error?`<p class="warning">${e(t.hardware_error)}</p>`:''}<p>Ein Profil pro Modellpaket. Aktivierte Profile sind für die Weboberfläche verfügbar. Gewichte laden bei Auftrag und werden danach entladen. Automatisch bevorzugt die RTX 5080, anschließend die RTX 3060; eine feste GPU wird niemals still gewechselt.</p><div class="detail-list">${s.models.map(m=>{const p=t.profiles[m.id];return `<details class="detail-row"><summary class="detail-summary"><strong>${e(m.name)}</strong><span>${p.enabled?'Aktiv':'Deaktiviert'}</span><span>${e(p.device==='auto'?'Automatisch · 5080 zuerst':t.devices.find(d=>d.uuid===p.device)?.name||'GPU nicht vorhanden')}</span></summary><div class="detail-body"><form data-sep-profile="${e(m.id)}"><label><input name="enabled" type="checkbox" ${p.enabled?'checked':''}> Für Audio-Trennung aktivieren</label><label>Ausführen auf<select name="device"><option value="auto" ${p.device==='auto'?'selected':''}>Automatisch · RTX 5080 zuerst</option>${t.devices.map(d=>`<option value="${e(d.uuid)}" ${p.device===d.uuid?'selected':''}>${e(d.name)}</option>`).join('')}${p.device!=='auto'&&!t.devices.some(d=>d.uuid===p.device)?`<option value="${e(p.device)}" selected>Gespeicherte GPU nicht vorhanden</option>`:''}</select></label><p>${m.installed?'Modellpaket bereit':'Modellpaket noch herunterladen'}</p><button ${t.job?.state==='running'?'disabled':''}>Profil speichern</button></form></div></details>`;}).join('')}</div>`;
}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>`;}
}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&&t.profiles[m.id]?.enabled).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.gpu?'· '+e(j.gpu):''} ${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);}
+35 -3
View File
@@ -1,14 +1,46 @@
"""Bounded WAV tests with coordinated GPU ownership and disposable input."""
import io,json,os,signal,subprocess,threading,time,uuid,wave
import io,json,os,re,signal,subprocess,threading,time,uuid,wave
from pathlib import Path
from image_test import probe
from separator_runtime import MODELS
def validate_profiles(settings):
if not isinstance(settings,dict) or set(settings)-set(MODELS):raise ValueError('Unbekanntes Audio-Trennprofil.')
for value in settings.values():
if not isinstance(value,dict) or set(value)!={'enabled','device'} or type(value['enabled']) is not bool or not isinstance(value['device'],str) or value['device']!='auto' and not re.fullmatch(r'GPU-[a-fA-F0-9-]{16,64}',value['device']):raise ValueError('Ungültiges Audio-Trennprofil.')
return settings
class SeparatorTests:
def __init__(self,root,runtime,scheduler):
self.root=Path(root);self.runtime=runtime;self.scheduler=scheduler;self.lock=threading.RLock();self.job=None;self.process=None;self.cancel=threading.Event();self.thread=None
def profiles(self):
path=self.root.parent/'separator-profiles.json'
settings=validate_profiles(json.loads(path.read_text())) if path.exists() else {}
return {name:dict(settings.get(name,dict(enabled=True,device='auto'))) for name in MODELS}
def configure(self,data):
if not isinstance(data,dict) or set(data)!={'model','enabled','device'} or data['model'] not in MODELS:raise ValueError('Modell, Aktivierung und GPU erforderlich.')
validate_profiles({data['model']:{k:data[k] for k in ('enabled','device')}})
if data['device']!='auto' and not any(g['uuid']==data['device'] for g in probe()):raise ValueError('Gewählte GPU ist nicht vorhanden.')
with self.lock:
if self.job and self.job['state']=='running':raise ValueError('Zuerst den laufenden Trennauftrag beenden.')
settings=self.profiles();settings[data['model']]={k:data[k] for k in ('enabled','device')}
from auth import atomic_write
atomic_write(self.root.parent/'separator-profiles.json',settings)
return self.status()
def status(self):
with self.lock:return dict(job=dict(self.job) if self.job else None)
try:devices=[dict(uuid=g['uuid'],name=g['name']) for g in probe()];hardware_error=None
except (OSError,ValueError,subprocess.SubprocessError):devices=[];hardware_error='GPU-Erkennung derzeit nicht verfügbar.'
with self.lock:return dict(job=dict(self.job) if self.job else None,profiles=self.profiles(),devices=devices,hardware_error=hardware_error)
def select_gpu(self,model):
settings=self.profiles()[model]
if not settings['enabled']:raise ValueError('Dieses Audio-Trennprofil ist deaktiviert. Unter Profile aktivieren.')
available=[g for g in probe() if not g['processes'] and g['free_mib']>4000]
if settings['device']!='auto':
gpu=next((g for g in available if g['uuid']==settings['device']),None)
if not gpu:raise ValueError('Die gewählte GPU ist nicht ausreichend frei. GPU-Auswahl im Profil prüfen; kein stiller Wechsel.')
return gpu
return next((g for term in ('RTX 5080','RTX 3060','') for g in available if term in g['name']),None)
def start(self,model,audio,target=None,full=False):
if isinstance(model,str) and model in MODELS and not self.profiles()[model]['enabled']:raise ValueError('Dieses Audio-Trennprofil ist deaktiviert. Unter Profile aktivieren.')
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.')
@@ -27,7 +59,7 @@ class SeparatorTests:
release=None;directory=self.root/self.job['id'];state='failed';phase='Trennung fehlgeschlagen.'
try:
release=self.scheduler.image_reservation(kind='separator')
gpu=next((g for term in ('RTX 3060','RTX 5080') for g in probe() if term in g['name'] and not g['processes'] and g['free_mib']>4000),None)
gpu=self.select_gpu(model)
if not gpu:raise ValueError('Keine ausreichend freie GPU. Fremde Dienste werden nicht beendet.')
directory.mkdir(mode=0o700);(directory/'input.wav').write_bytes(audio);diagnostics=(directory/'worker.tmp').open('wb')
with self.lock:
+3
View File
@@ -539,6 +539,9 @@ class Handler(BaseHTTPRequestHandler):
if data:raise ValueError('Keine Parameter erwartet.')
return self.respond(self.server.chat_tests.stop() if self.path.endswith('/cancel') else self.server.chat_tests.unload())
except ValueError as exc:return self.respond({'error':str(exc)},400)
if self.path == '/api/v1/separator-profiles/save':
try:return self.respond(self.server.separator_tests.configure(self.read_json()))
except (ValueError,OSError) as exc:return self.respond({'error':str(exc)},400)
if self.path in ('/api/v1/separator-runtime/install','/api/v1/separator-runtime/cancel','/api/v1/separator-runtime/download'):
try:
data=self.read_json()
+7
View File
@@ -110,6 +110,13 @@ class RestoreTests(unittest.TestCase):
summary=self.server.backup.inspect(seal(doc,'backup secure password'),'backup secure password')
self.assertTrue(summary['services'][0]['restorable'])
self.assertFalse(any('Buildquelle fehlt' in w for w in summary['warnings']))
def test_separator_profile_settings_roundtrip(self):
from separator_runtime import MODELS
name=next(iter(MODELS));settings={name:dict(enabled=False,device='GPU-'+'a'*32)}
(self.root/'separator-profiles.json').write_text(json.dumps(settings))
doc=self.server.backup.snapshot(include_history=False);validate(doc)
(self.root/'separator-profiles.json').unlink();self.server.backup._apply_settings(doc)
self.assertEqual(self.server.separator_tests.profiles()[name],settings[name])
def test_rejects_unsafe_paths_and_bad_references_before_changes(self):
ident=self.model();self.profile(ident);doc=self.server.backup.snapshot(include_history=False)
for change in (lambda d:d['models'][0].update(file='../secret'),lambda d:d['settings']['profiles.json'][0].update(model_id='f'*64),lambda d:d.update(version=99)):
+13
View File
@@ -61,4 +61,17 @@ class Tests(unittest.TestCase):
worker=SeparatorTests(root,Mock(),scheduler)
with self.assertRaisesRegex(ValueError,'Steuerung'):worker.start(next(iter(MODELS)),audio(),full=True)
def test_profiles_prefer_5080_and_fixed_device_never_falls_back(self):
with tempfile.TemporaryDirectory() as root:
worker=SeparatorTests(Path(root)/'separator-tests',Mock(),Mock());model=next(iter(MODELS))
fast={'name':'RTX 5080','uuid':'GPU-'+'a'*32,'processes':0,'free_mib':12000}
slow={'name':'RTX 3060','uuid':'GPU-'+'b'*32,'processes':0,'free_mib':11000}
with patch('separator.probe',return_value=[slow,fast]):
self.assertEqual(worker.select_gpu(model)['uuid'],fast['uuid'])
worker.configure(dict(model=model,enabled=True,device=fast['uuid']))
with patch('separator.probe',return_value=[slow,dict(fast,processes=1)]):
with self.assertRaisesRegex(ValueError,'kein stiller Wechsel'):worker.select_gpu(model)
with patch('separator.probe',return_value=[fast]):worker.configure(dict(model=model,enabled=False,device='auto'))
restored=SeparatorTests(Path(root)/'separator-tests',Mock(),Mock())
with self.assertRaisesRegex(ValueError,'deaktiviert'):restored.start(model,audio())
if __name__=='__main__':unittest.main()