Add profile presets and min-p controls, embeddings and VoxCPM workers

This commit is contained in:
Mikei386 committed 2026-10-02 18:42:29 +02:00
1 parent 74dd2ed8ba
commit a7b30b62d2
32 files changed
+615 -64

No files matched your search

+48 -8
View File
@@ -8,10 +8,10 @@ from pathlib import Path
from catalog import file_role
SCHEMAS={
'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40),'repeat_penalty':(0,2,1.0),'presence_penalty':(-2,2,0.0),'frequency_penalty':(-2,2,0.0),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)},
'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40),'min_p':(0,1,0.0),'repeat_penalty':(0,2,1.0),'presence_penalty':(-2,2,0.0),'frequency_penalty':(-2,2,0.0),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)},
'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)},
'stt':{},'music':{},'voice':{},
'stt':{},'music':{},'voice':{},'embeddings':{},
'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)}
}
VIDEO_DEVICE_DEFAULTS={'video_device':'auto','text_encoder_device':'same'}
@@ -26,8 +26,8 @@ def video_parameters(params):
return params
CHAT_GPU_DEFAULTS={'gpu_devices':[], 'split_mode':'none', 'tensor_split':[], 'mtp':False,'gpu_offload':'auto','gpu_reserve_mode':'auto','gpu_reserve_mib':{},'vision_projector':None,'vision_device':'cpu','cache_type_k':'q4_0','cache_type_v':'q4_0'}
FLOAT_PARAMETERS=frozenset({'guidance','speed','temperature','top_p','mtp_min_p','repeat_penalty','presence_penalty','frequency_penalty'})
CHAT_GENERATION_DEFAULTS={'temperature':.8,'top_p':.95,'top_k':40,'repeat_penalty':1.0,'presence_penalty':0.0,'frequency_penalty':0.0}
FLOAT_PARAMETERS=frozenset({'guidance','speed','temperature','top_p','min_p','mtp_min_p','repeat_penalty','presence_penalty','frequency_penalty'})
CHAT_GENERATION_DEFAULTS={'temperature':.8,'top_p':.95,'top_k':40,'min_p':0.0,'repeat_penalty':1.0,'presence_penalty':0.0,'frequency_penalty':0.0}
CHAT_SAMPLING={**CHAT_GENERATION_DEFAULTS,'mtp_tokens':2,'mtp_min_p':.05}
def generation_parameters(params):
@@ -145,6 +145,17 @@ class Profiles:
self.prompt_enhancer_ready=lambda task:False
self.video_blockers=None;self.chat_blockers=None;self.tts_blockers=None;self.stt_blockers=None
self.rows=json.loads(self.path.read_text()) if self.path.exists() else []
# First baselines may be captured while a pre-Preset server still runs.
# Merge on startup so its old in-memory saves cannot discard that baseline.
pending=self.path.with_name('profile-presets-pending.json')
if pending.exists():
for item in json.loads(pending.read_text()):
row=next((p for p in self.rows if p['id']==item['profile_id']),None)
if row is not None and not any(x['name']==item['preset']['name'] for x in row.get('presets',[])):
row.setdefault('presets',[]).append(item['preset'])
temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(self.rows));temp.replace(self.path)
pending.unlink()
def status(self):
with self.lock:
rows=json.loads(json.dumps(self.rows))
@@ -156,6 +167,7 @@ class Profiles:
p['model']=self.catalog.entry(p['model_id'])
if p['kind']=='image':p['capabilities']=image_capabilities(p['model'])
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
if p['kind']=='embeddings':p['blockers']=['Embedding-Worker noch nicht angebunden. Dieses Profil erzeugt Vektoren und wird nicht als Chat-Modell angeboten.']
if p['kind']=='image' and image_recipe(p['model']):
p['blockers']=['Eine eigene Bildlaufzeit muss eingerichtet werden; llama.cpp führt keine Bildmodelle aus.']
for role,info in reversed(list(image_recipe(p['model']).items())):
@@ -173,7 +185,7 @@ class Profiles:
try:self._component(p['model'],role,p.get('components',{}).get(role))
except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.')
if p.get('model') and voice_recipe(p['model']):
p['blockers']=['VoxCPM2-Ausführung fehlt: Die VoxCPM-Laufzeit und der Deck-Worker sind noch nicht angebunden.']
p['blockers']=[] if getattr(self,'voxcpm_runtime_ready',lambda:False)() else ['VoxCPM-Laufzeit unter Laufzeiten → VoxCPM installieren.']
for role,info in voice_recipe(p['model']).items():
try:self._component(p['model'],role,p.get('components',{}).get(role))
except ValueError:p['blockers'].append(info['label']+' fehlt oder ist noch nicht zugeordnet.')
@@ -181,6 +193,7 @@ class Profiles:
if p['kind']=='audio' and self.tts_blockers and p.get('model') and not voice_recipe(p['model']):p['blockers']=self.tts_blockers(p)
if p['kind']=='music' and getattr(self,'music_blockers',None) and p.get('model'):p['blockers']=self.music_blockers(p)
if p['kind']=='stt' and self.stt_blockers and p.get('model'):p['blockers']=self.stt_blockers(p)
if p['kind']=='embeddings' and getattr(self,'embedding_blockers',None) and p.get('model'):p['blockers']=self.embedding_blockers(p)
if p['kind']=='chat' and self.chat_blockers and p.get('model'):p['blockers']=self.chat_blockers(p)
if p['kind']=='image' and (p['parameters']['width']>1024 or p['parameters']['height']>1024):p['blockers'].append('Die Bildlaufzeit unterstützt derzeit maximal 1024 × 1024 Pixel.')
if p['kind']=='image':
@@ -219,17 +232,41 @@ class Profiles:
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.')
components=existing.get('components',{}) if existing and existing['model_id']==model['id'] else {}
row=dict(components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,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())
row=dict(presets=existing.get('presets',[]) if existing else [],components=components,prompt_enhancer=existing.get('prompt_enhancer',dict(PROMPT_ENHANCER_DEFAULTS)) if existing and existing['model_id']==model['id'] and kind=='image' else dict(PROMPT_ENHANCER_DEFAULTS) if kind=='image' else None,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
def preset(self,data):
if not isinstance(data,dict) or data.get('action') not in ('save','delete'):raise ValueError('Ungültige Preset-Aktion.')
required={'id','revision','action','name' if data['action']=='save' else 'preset_id'}
if set(data)!=required:raise ValueError('Ungültige Preset-Felder.')
with self.lock:
old=next((p for p in self.rows if p['id']==data['id']),None)
if not old or old['revision']!=data['revision']:raise ValueError('Profil geändert. Ansicht neu laden.')
presets=json.loads(json.dumps(old.get('presets',[])))
if data['action']=='save':
name=data['name']
if not isinstance(name,str) or not 1<=len(name.strip())<=64 or any(ord(c)<32 for c in name):raise ValueError('Preset-Name: 1–64 Zeichen.')
name=name.strip()
if any(x['name']==name for x in presets):raise ValueError('Preset-Name bereits vergeben.')
if len(presets)>=50:raise ValueError('Maximal 50 Presets pro Profil.')
presets.append(dict(id=uuid.uuid4().hex,name=name,created_at=time.time(),model_id=old['model_id'],parameters=json.loads(json.dumps(old['parameters']))))
else:
if not any(x['id']==data['preset_id'] for x in presets):raise ValueError('Preset nicht gefunden.')
presets=[x for x in presets if x['id']!=data['preset_id']]
updated=dict(old,presets=presets,revision=old['revision']+1,updated_at=time.time())
rows=[updated if p is old else p for p in self.rows]
temp=self.path.with_suffix('.tmp');temp.write_text(json.dumps(rows));temp.replace(self.path);self.rows=rows
return updated
def references(self,model_id):
with self.lock:
return [dict(id=p['id'],name=p['name'],kind=p['kind']) for p in self.rows
if model_id in (p.get('model_id'),p.get('parameters',{}).get('vision_projector'))
or model_id in (p.get('components') or {}).values()]
or model_id in (p.get('components') or {}).values()
or any(model_id in (x.get('model_id'),x.get('parameters',{}).get('vision_projector')) for x in p.get('presets',[]))]
def configure_prompt_enhancer(self,data):
if not isinstance(data,dict) or set(data)!={'id','revision','prompt_enhancer'}:raise ValueError('Ungültige Prompt-Aufwerter-Einstellung.')
@@ -267,6 +304,9 @@ class Profiles:
return item
def components(self,model_id,extra_repo=None):
model=self.catalog.entry(model_id)
from embeddings import supported
if supported(model):return dict(supported=True,requirements=[],source='https://huggingface.co/'+model['repo'],license='gemma',runtime='llama.cpp · CPU · EmbeddingGemma',runtime_route='#runtime',runtime_installed=not bool(getattr(self,'embedding_blockers',lambda p:['Laufzeit prüfen'])({'model':model})),no_components=True,message='Alles enthalten: Tokenizer und Konfiguration liegen in der GGUF-Datei. Kein separater Textencoder, VAE oder Projektor nötig. Die vorhandene llama.cpp-Laufzeit wird auf CPU verwendet. Profil speichern und am API-Endpunkt freigeben; Vektoren über /v1/embeddings abrufen.')
if model.get('kind')=='embeddings':return dict(supported=False,requirements=[],message='Für diese Embedding-Datei fehlt ein geprüftes Rezept. Zusatzdateien werden nicht pauschal verlangt. Unterstützt: ggml-org/embeddinggemma-300m-qat-q8_0-GGUF.')
if extra_repo or not self._recipe(model):return self.manual_components(model,extra_repo)
recipe=self._recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) or voice_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
for role,info in recipe.items():
@@ -277,7 +317,7 @@ class Profiles:
except ValueError:pass
candidates=[dict(f,repo=source.get('repo',info.get('repo',model['repo'])),revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for name in info['files'] for f in source['files'] if f['name']==name]
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
if voice_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='VoxCPM · eigene Python/PyTorch-Laufzeit',runtime_installed=bool(getattr(self,'voxcpm_runtime_ready',lambda:False)()),runtime_route='#voxcpm-runtime',worker_available=False,message='VoxCPM2 benötigt genau diese fünf Zusatzdateien derselben Quellversion. Ein separater Textencoder und ein Vision-Projektor werden nicht benötigt. tokenizer.json und tokenizer_config.json gehören zusammen, sie sind keine doppelten Modelle. Vorhandene Dateien werden wiederverwendet. Die passende Laufzeit kann unter Einstellungen → Laufzeiten → VoxCPM installiert werden. Der Deck-Worker fehlt noch; ein vollständiges Dateipaket allein kann noch nicht gestartet werden.')
if voice_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='VoxCPM · eigene Python/PyTorch-Laufzeit',runtime_installed=bool(getattr(self,'voxcpm_runtime_ready',lambda:False)()),runtime_route='#voxcpm-runtime',worker_available=True,message='VoxCPM2 benötigt genau diese fünf Zusatzdateien derselben Quellversion. Ein separater Textencoder und ein Vision-Projektor werden nicht benötigt. tokenizer.json und tokenizer_config.json gehören zusammen, sie sind keine doppelten Modelle. Vorhandene Dateien werden wiederverwendet. Die passende Laufzeit kann unter Einstellungen → Laufzeiten → VoxCPM installiert werden. Nach Zuordnung dieser Dateien unter Stimmwerkzeuge → Testen Sprache erzeugen oder eine WAV-Referenz für Stimmklonen verwenden.')
if music_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='audio.cpp',runtime_route='#audio-cpp-runtime',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Passendes YuE2-GGUF-Paket: je eine VAE-Datei und die vier Zusatzdateien derselben Quellversion. F16-VAE ist die erste Downloadempfehlung. Vorhandene Dateien wiederverwenden, fehlende herunterladen und danach zuordnen. Die YuE2-Musikanbindung verwendet diese Dateien über audio.cpp. Speichern startet kein Modell; anschließend Musik → Testen verwenden.')
if video:return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='ComfyUI · gemeinsame Bild- und Videolaufzeit',runtime_route='#image-runtime',runtime_installed=bool(getattr(self,'video_runtime_ready',lambda:False)()),message='Geprüftes Dateirezept nach Herstellerdokumentation für die zweistufige Distilled-Pipeline mit fester Bildanzahl. Alle Komponenten stammen aus derselben Version wie dein Transformer. Kein beliebiger Gemma-Encoder; keine Comfy-INT8-Dateien. Optionaler Duration-Head, Temporal-Upsampling und DFR sind nicht Teil dieses Rezepts. Vorhandene ComfyUI-Laufzeit wiederverwenden; Videomodell im Bereich Video aktivieren. Vollständige Dateien sind keine Speicherzusage.')
if not image_recipe(model):return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='audio.cpp · Paketdefinition',runtime_installed=bool(getattr(self,'music_runtime_ready',lambda:False)()),message='Zusatzdateien aus der installierten Laufzeit-Paketdefinition. Weitere modellabhängige Pakete können erforderlich sein; dies ist keine vollständige Worker-Zusage.')