Persist library profiles and download history with weight capacity checks

This commit is contained in:
Mikei386
2026-09-28 17:26:21 +02:00
parent f326de18d1
commit 458047a1d3
15 changed files with 360 additions and 180 deletions
+53
View File
@@ -0,0 +1,53 @@
"""Server-persisted library-backed profiles with validated, typed parameters."""
import json
import re
import threading
import time
import uuid
from pathlib import Path
SCHEMAS={
'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128)},
'image':{'width':(256,2048,1024),'height':(256,2048,1024),'steps':(1,100,25),'seed':(-1,2147483647,-1),'guidance':(0,30,1)},
'audio':{'speed':(.25,4,1)},
'video':{'width':(256,1920,768),'height':(256,1088,512),'frames':(1,241,33),'fps':(1,60,24),'steps':(1,100,20),'seed':(-1,2147483647,-1)}
}
class Profiles:
def __init__(self,path,catalog):
self.path=Path(path);self.catalog=catalog;self.lock=threading.RLock()
self.rows=json.loads(self.path.read_text()) if self.path.exists() else []
def status(self):
with self.lock:
rows=json.loads(json.dumps(self.rows))
for p in rows:
try:
p['model']=self.catalog.entry(p['model_id'])
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
if p['kind']=='image' and 'qwen-image-2.1' in p['model']['repo'].lower():
p['blockers']=['Qwen-Image-2.1 benötigt zusätzlich einen kompatiblen Textencoder und VAE.','Eine eigene Bildlaufzeit muss eingerichtet werden; llama.cpp führt keine Bildmodelle aus.']
except ValueError as exc:p['model']=None;p['blockers']=[str(exc)]
p['state']='configured';p['runnable']=False
return {'profiles':rows,'schemas':SCHEMAS}
def save(self,data):
if set(data)!={'id','revision','name','kind','model_id','parameters'}:raise ValueError('Ungültige Profilfelder.')
kind=data['kind'];name=data['name'];params=data['parameters']
if not isinstance(kind,str) or kind not in SCHEMAS or not isinstance(name,str) or not re.fullmatch('[a-zA-Z0-9][a-zA-Z0-9_-]{1,63}',name):raise ValueError('API-Name: 2–64 Buchstaben, Ziffern, Bindestrich oder Unterstrich.')
model=self.catalog.entry(data['model_id'])
if model['kind']!=kind or not model['file'].lower().endswith(('.gguf','.safetensors')):raise ValueError('Eine Gewichtsdatei dieses Bereichs auswählen, keine Konfiguration.')
if not isinstance(params,dict) or set(params)!=set(SCHEMAS[kind]):raise ValueError('Unvollständige oder unbekannte Profilparameter.')
for key,(lo,hi,_) in SCHEMAS[kind].items():
value=params[key]
floating=key in ('guidance','speed')
if isinstance(value,bool) or not isinstance(value,(float,int) if floating else int) or not lo<=value<=hi:raise ValueError('Ungültiger Parameter: '+key)
if kind in ('image','video') and (params['width']%64 or params['height']%64):raise ValueError('Breite und Höhe müssen durch 64 teilbar sein.')
if kind=='chat' and params['ubatch']>params['batch']:raise ValueError('Microbatch darf nicht größer als Batch sein.')
with self.lock:
existing=next((p for p in self.rows if p['id']==data['id']),None)
if data['id'] is not None and not existing:raise ValueError('Profil nicht gefunden.')
if data['revision']!=(existing['revision'] if existing else 0):raise ValueError('Profil wurde zwischenzeitlich geändert. Ansicht neu laden.')
if any(p['name']==name and p is not existing for p in self.rows):raise ValueError('API-Profilname bereits vergeben.')
row=dict(id=existing['id'] if existing else uuid.uuid4().hex,revision=data['revision']+1,name=name,kind=kind,model_id=model['id'],parameters=params,updated_at=time.time())
rows=[row if p is existing else p for p in self.rows] if existing else self.rows+[row]
self.path.parent.mkdir(parents=True,exist_ok=True,mode=0o700)
temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(rows));temp.replace(self.path);self.rows=rows
return row