Unify component setup across categories and retain two installation backups
This commit is contained in:
+39
-7
@@ -5,6 +5,7 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
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),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)},
|
||||
@@ -92,7 +93,22 @@ def music_recipe(model):
|
||||
def component_recipe(model):
|
||||
return music_recipe(model) or video_recipe(model) or image_recipe(model)
|
||||
|
||||
MANUAL_ROLES={'manual_text_encoder':'Textencoder (ungeprüft)','manual_vae':'VAE (ungeprüft)','manual_tokenizer':'Tokenizer (ungeprüft)','manual_configuration':'Konfiguration (ungeprüft)','manual_projector':'Projektor (ungeprüft)','manual_auxiliary':'Weitere Komponente (ungeprüft)'}
|
||||
|
||||
def audio_package_recipe(model,source):
|
||||
if not source or not model.get('repo','').startswith('audio-cpp/'):return None
|
||||
for path in sorted(Path(source,'model_specs').glob('*.json')):
|
||||
try:spec=json.loads(path.read_text())
|
||||
except (ValueError,OSError):continue
|
||||
for package in spec.get('packages',[]):
|
||||
files=package.get('files',[])
|
||||
if package.get('target_directory')!=model['repo'].split('/')[-1] or model['file'] not in files:continue
|
||||
extras=[f for f in files if isinstance(f,str) and f!=model['file'] and not f.startswith('/') and '..' not in Path(f).parts]
|
||||
return {f'runtime_file_{i}':dict(label='Laufzeit-Paket: '+f,files=[f]) for i,f in enumerate(extras)} or None
|
||||
return None
|
||||
|
||||
class Profiles:
|
||||
def _recipe(self,model):return component_recipe(model) or getattr(self,'runtime_recipe',lambda m:None)(model)
|
||||
def __init__(self,path,catalog):
|
||||
self.path=Path(path);self.catalog=catalog;self.lock=threading.RLock()
|
||||
self.image_runtime_ready=lambda:False
|
||||
@@ -134,6 +150,7 @@ class Profiles:
|
||||
if p['kind']=='image':
|
||||
for task,repo in PROMPT_ENHANCER_REPOS.items():
|
||||
if p['prompt_enhancer'].get(task)==repo and not self.prompt_enhancer_ready(task):p['blockers'].append('Prompt-Aufwerter '+task.upper()+' fehlt. Unter Bildgenerierung → Aufwerter installieren.')
|
||||
if any(k in MANUAL_ROLES for k in p.get('components',{})):p['blockers'].append('Manuelle Zusatzkomponenten sind ungeprüft und noch nicht an den Worker angebunden.')
|
||||
p['runnable']=not p['blockers'];p['state']='ready' if p['runnable'] else 'configured'
|
||||
return {'profiles':[p for p in rows if p['kind']!='video'],'schemas':{k:v for k,v in SCHEMAS.items() if k!='video'}}
|
||||
def save(self,data):
|
||||
@@ -202,16 +219,20 @@ class Profiles:
|
||||
return {'deleted':old['id']}
|
||||
|
||||
def _component(self,model,role,ident):
|
||||
recipe=component_recipe(model)
|
||||
if role in MANUAL_ROLES:
|
||||
item=self.catalog.entry(ident)
|
||||
if item['kind']!=model['kind'] or item['id']==model['id']:raise ValueError('Andere Zusatzdatei aus derselben Kategorie wählen.')
|
||||
return item
|
||||
recipe=self._recipe(model)
|
||||
if not recipe or role not in recipe:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.')
|
||||
item=self.catalog.entry(ident)
|
||||
if item['repo']!=recipe[role].get('repo',model['repo']) or item['kind']!=model['kind'] or item['file'] not in recipe[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.')
|
||||
if (video_recipe(model) or music_recipe(model)) and item['revision']!=model['revision']:raise ValueError('Komponente muss dieselbe Quellversion wie das Hauptmodell verwenden.')
|
||||
return item
|
||||
def components(self,model_id):
|
||||
def components(self,model_id,extra_repo=None):
|
||||
model=self.catalog.entry(model_id)
|
||||
if not component_recipe(model):return dict(supported=False,requirements=[],message='Für dieses Modell ist noch kein Komponentenrezept hinterlegt. Keine automatische Zuordnung.')
|
||||
recipe=component_recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
|
||||
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) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
|
||||
for role,info in recipe.items():
|
||||
available=[]
|
||||
for item in entries:
|
||||
@@ -222,16 +243,27 @@ class Profiles:
|
||||
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
|
||||
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_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. Der Deck-Musikworker und die Ladypoly-Anbindung fehlen weiterhin; vollständige Komponenten starten kein Modell.')
|
||||
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_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.')
|
||||
return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime='ComfyUI + ComfyUI-GGUF',runtime_installed=self.image_runtime_ready(),
|
||||
message='Fehlende Dateien herunterladen, danach Bibliothek aktualisieren und zuordnen. Für FLUX.2 Klein empfehlen sich zum Einstieg 4 Schritte und Guidance 1. Die Bildgewichte werden bei Bedarf zwischen RAM und RTX 5080 verschoben.' if model['repo']==FLUX_REPO else 'Zusatzdateien laut Modellkarte für ComfyUI. Kompatibilität und Speicherbedarf müssen mit der eingerichteten Laufzeit noch praktisch geprüft werden.')
|
||||
def manual_components(self,model,repo=None):
|
||||
source=self.catalog.files(repo or model['repo'],None if repo else model['revision'])
|
||||
entries=self.catalog.status()['entries'];requirements=[]
|
||||
for role,label in MANUAL_ROLES.items():
|
||||
match={'manual_text_encoder':('text_encoder',),'manual_vae':('vae',),'manual_projector':('vision_projector','audio_projector'),'manual_configuration':('configuration',),'manual_auxiliary':('auxiliary',),'manual_tokenizer':('configuration','auxiliary')}[role]
|
||||
def matches(name):return name.endswith('.tiktoken') or 'tokenizer' in name.lower() if role=='manual_tokenizer' else file_role(name)['role'] in match
|
||||
candidates=[dict(f,repo=source['repo'],revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for f in source['files'] if f['name']!=model['file'] and matches(f['name'])]
|
||||
available=[x for x in entries if x['kind']==model['kind'] and x['id']!=model['id'] and matches(x['file'])]
|
||||
requirements.append(dict(role=role,label=label,available=available,candidates=candidates,optional=True))
|
||||
return dict(supported=True,manual=True,requirements=requirements,source=source['url'],license=source['license'],runtime='Siehe Laufzeiterkennung am Modell',runtime_installed=False,message='Kein vollständiges geprüftes Rezept für diese Auswahl. Diese Dateien sind Hinweise aus Dateinamen und keine bestätigten Pflichtkomponenten. Manuell ausgewählte Dateien werden als ungeprüft gespeichert und schalten keinen Worker frei.')
|
||||
def assign(self,data):
|
||||
if set(data)!={'id','revision','components'} or not isinstance(data['components'],dict):raise ValueError('Ungültige Komponentenzuordnung.')
|
||||
with self.lock:
|
||||
old=next((p for p in self.rows if p['id']==data['id']),None)
|
||||
if not old or old['kind'] not in ('image','video','music') or old['revision']!=data['revision']:raise ValueError('Profil geändert oder nicht gefunden. Ansicht neu laden.')
|
||||
if not old or old['kind'] not in SCHEMAS or old['revision']!=data['revision']:raise ValueError('Profil geändert oder nicht gefunden. Ansicht neu laden.')
|
||||
model=self.catalog.entry(old['model_id'])
|
||||
if set(data['components'])-set(component_recipe(model) or {}):raise ValueError('Unbekannte Komponente für dieses Modell.')
|
||||
components={k:v for k,v in data['components'].items() if v}
|
||||
if set(data['components'])-(set(self._recipe(model) or {})|set(MANUAL_ROLES)):raise ValueError('Unbekannte Komponente für dieses Modell.')
|
||||
components={k:v for k,v in {**old.get('components',{}),**data['components']}.items() if v}
|
||||
for role,ident in components.items():self._component(model,role,ident)
|
||||
updated=dict(old,components=components,revision=old['revision']+1,updated_at=time.time())
|
||||
rows=[updated if p is old else p for p in self.rows]
|
||||
|
||||
Reference in New Issue
Block a user