Add profile presets and min-p controls, embeddings and VoxCPM workers
This commit is contained in:
1 parent
74dd2ed8ba
commit
a7b30b62d2
32 files changed
+615
-64
No files matched your search
+48
-8
@@ -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.')
|
||||
|
||||
Reference in new issue
Block a user