Add separation profiles with activation and explicit GPU assignment
This commit is contained in:
1 parent
356abbeb9d
commit
932284a831
8 files changed
+82
-8
No files matched your search
@@ -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.
|
||||
@@ -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.')
|
||||
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user