Centralize image family runtime and workflow adapters
This commit is contained in:
@@ -232,3 +232,5 @@ Installation, Verbindung und Einschränkungen: [LadyPoly](deploy/ladypoly/README
|
|||||||
Sprachmodell-GPUs und Vision-Projektor werden unabhängig zugeordnet. Der Modell-Split verteilt ausschließlich LLM-Gewichte; eine zusätzliche Projektor-GPU wird nur für mmproj sichtbar gemacht, nicht für Modell-Offload. Die Speicherprüfung berücksichtigt beide Geräte einschließlich Projektorbedarf.
|
Sprachmodell-GPUs und Vision-Projektor werden unabhängig zugeordnet. Der Modell-Split verteilt ausschließlich LLM-Gewichte; eine zusätzliche Projektor-GPU wird nur für mmproj sichtbar gemacht, nicht für Modell-Offload. Die Speicherprüfung berücksichtigt beide Geräte einschließlich Projektorbedarf.
|
||||||
|
|
||||||
Referenzbilder: Alle derzeit ausführbaren Bildrezepte (Qwen Image 2.1 und FLUX.2 Klein 9B) erlauben bis zu vier Bilder pro Bearbeitungsauftrag. FLUX nutzt VAEEncode und verkettete ReferenceLatent-Nodes für beide Conditioning-Zweige nach dem offiziellen ComfyUI-Workflow: https://github.com/Comfy-Org/workflow_templates/blob/main/templates/image_flux2_klein_image_edit_9b_distilled.json . Neue Modellfamilien benötigen einen passenden Workflow; hochgeladene Referenzen werden niemals stillschweigend verworfen.
|
Referenzbilder: Alle derzeit ausführbaren Bildrezepte (Qwen Image 2.1 und FLUX.2 Klein 9B) erlauben bis zu vier Bilder pro Bearbeitungsauftrag. FLUX nutzt VAEEncode und verkettete ReferenceLatent-Nodes für beide Conditioning-Zweige nach dem offiziellen ComfyUI-Workflow: https://github.com/Comfy-Org/workflow_templates/blob/main/templates/image_flux2_klein_image_edit_9b_distilled.json . Neue Modellfamilien benötigen einen passenden Workflow; hochgeladene Referenzen werden niemals stillschweigend verworfen.
|
||||||
|
|
||||||
|
Bildadapter werden zentral in `profiles.IMAGE_ADAPTERS` definiert: Modellfamilie, unterstütztes Gewichtsformat, Komponenten, Laufzeit, Workflow, Referenzbildlimit und Speicherpolitik. Bibliothek, Profile und Bildworker verwenden denselben Resolver `image_adapter`. Bekannte Quellen bleiben kompatibel; neue Varianten können über gespeicherte präzise Architektur-Metadaten oder die Hugging-Face-Basismodell-Zuordnung dieselbe Anbindung wiederverwenden. Ein Name mit „Qwen“ oder „Flux“ genügt nicht. Allgemeine FLUX-Architekturangaben unterscheiden Klein 9B nicht sicher von anderen FLUX-Varianten und schalten daher keinen Worker frei. Metadaten sind eine Zuordnungshilfe, keine Garantie für den Inhalt oder die Qualität veränderter Gewichte. Neue Architekturen und Dateiformate benötigen einen registrierten Adapter. Bereits heruntergeladene Varianten ohne ausreichende Metadaten bleiben ungeprüft; bestehende bekannte Profile bleiben erhalten.
|
||||||
|
|||||||
+5
-1
@@ -89,9 +89,13 @@ def runtime_metadata(model):
|
|||||||
architecture=next((x for x in architectures if isinstance(x,str) and 0<len(x)<=100),None)
|
architecture=next((x for x in architectures if isinstance(x,str) and 0<len(x)<=100),None)
|
||||||
architecture=architecture or next((config.get(k) for k in ('_class_name','model_type') if isinstance(config.get(k),str) and 0<len(config[k])<=100),None)
|
architecture=architecture or next((config.get(k) for k in ('_class_name','model_type') if isinstance(config.get(k),str) and 0<len(config[k])<=100),None)
|
||||||
tags=[x for x in model.get('tags',[]) if isinstance(x,str) and len(x)<=80][:32] if isinstance(model.get('tags'),list) else []
|
tags=[x for x in model.get('tags',[]) if isinstance(x,str) and len(x)<=80][:32] if isinstance(model.get('tags'),list) else []
|
||||||
|
card=model.get('cardData') if isinstance(model.get('cardData'),dict) else {}
|
||||||
|
base=card.get('base_model',[])
|
||||||
|
base=[base] if isinstance(base,str) else base if isinstance(base,list) else []
|
||||||
|
base_models=[x for x in base if isinstance(x,str) and re.fullmatch(r'[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+',x)][:8]
|
||||||
return dict(pipeline_tag=model.get('pipeline_tag') if isinstance(model.get('pipeline_tag'),str) and len(model['pipeline_tag'])<=80 else None,
|
return dict(pipeline_tag=model.get('pipeline_tag') if isinstance(model.get('pipeline_tag'),str) and len(model['pipeline_tag'])<=80 else None,
|
||||||
library_name=model.get('library_name') if isinstance(model.get('library_name'),str) and len(model['library_name'])<=80 else None,
|
library_name=model.get('library_name') if isinstance(model.get('library_name'),str) and len(model['library_name'])<=80 else None,
|
||||||
architecture=architecture,tags=tags)
|
architecture=architecture,tags=tags,base_models=base_models)
|
||||||
|
|
||||||
class Catalog:
|
class Catalog:
|
||||||
def __init__(self,root):
|
def __init__(self,root):
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
"""Read-only, conservative mapping to Deck's maintained execution adapters."""
|
"""Read-only, conservative mapping to Deck's maintained execution adapters."""
|
||||||
from catalog import file_role
|
from catalog import file_role
|
||||||
from stt import supported as stt_supported
|
from stt import supported as stt_supported
|
||||||
from profiles import image_recipe, video_recipe
|
from profiles import image_recipe, image_adapter, video_recipe
|
||||||
from tts_runtime import REPO, REVISION
|
from tts_runtime import REPO, REVISION
|
||||||
|
|
||||||
def _assess(model, installed, profile=None):
|
def _assess(model, installed, profile=None):
|
||||||
@@ -16,8 +16,8 @@ def _assess(model, installed, profile=None):
|
|||||||
result['family']='Qwen3-ASR 0.6B'
|
result['family']='Qwen3-ASR 0.6B'
|
||||||
result.update(runtime='llama.cpp · Qwen3-ASR auf CPU',route='#stt-runtime',components=['Passender Audio-Projektor (unter Einrichten herunterladen)'])
|
result.update(runtime='llama.cpp · Qwen3-ASR auf CPU',route='#stt-runtime',components=['Passender Audio-Projektor (unter Einrichten herunterladen)'])
|
||||||
elif kind=='image' and image_recipe(model):
|
elif kind=='image' and image_recipe(model):
|
||||||
result['family']='FLUX.2 Klein 9B' if 'FLUX' in filename.upper() else 'Qwen Image 2.1'
|
result['family']=image_adapter(model)['label']
|
||||||
result.update(runtime='ComfyUI + ComfyUI-GGUF',route='#image-runtime',components=[x['label'] for x in image_recipe(model).values()])
|
result.update(runtime=image_adapter(model)['runtime'],route='#image-runtime',components=[x['label'] for x in image_recipe(model).values()])
|
||||||
elif kind=='audio' and repo==REPO and filename=='model.safetensors' and model.get('revision')==REVISION:
|
elif kind=='audio' and repo==REPO and filename=='model.safetensors' and model.get('revision')==REVISION:
|
||||||
result['family']='Qwen3-TTS CustomVoice'
|
result['family']='Qwen3-TTS CustomVoice'
|
||||||
result.update(runtime='Qwen3-TTS CustomVoice',route='#tts-runtime',components=['Vollständiges Modellpaket und Sprach-Tokenizer (im Einrichtungsassistenten enthalten)'])
|
result.update(runtime='Qwen3-TTS CustomVoice',route='#tts-runtime',components=['Vollständiges Modellpaket und Sprach-Tokenizer (im Einrichtungsassistenten enthalten)'])
|
||||||
|
|||||||
+5
-5
@@ -13,7 +13,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import uuid
|
import uuid
|
||||||
from profiles import QWEN_REPO, FLUX_REPO, image_recipe, image_capabilities
|
from profiles import image_adapter, image_recipe, image_capabilities
|
||||||
|
|
||||||
PYTHON=Path('/opt/deck-image-python/bin/python')
|
PYTHON=Path('/opt/deck-image-python/bin/python')
|
||||||
COMFY=Path('/opt/deck-comfy')
|
COMFY=Path('/opt/deck-comfy')
|
||||||
@@ -160,11 +160,11 @@ class ImageTests:
|
|||||||
encoder=self.profiles._component(model,'text_encoder',profile.get('components',{}).get('text_encoder'))
|
encoder=self.profiles._component(model,'text_encoder',profile.get('components',{}).get('text_encoder'))
|
||||||
vae=self.profiles._component(model,'vae',profile.get('components',{}).get('vae'))
|
vae=self.profiles._component(model,'vae',profile.get('components',{}).get('vae'))
|
||||||
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith(('MemAvailable:','MemTotal:'))}
|
mem={line.split(':')[0]:int(line.split()[1])*1024 for line in Path('/proc/meminfo').read_text().splitlines() if line.startswith(('MemAvailable:','MemTotal:'))}
|
||||||
required_ram=max(20*GIB,encoder['size']*2+4*GIB,model['size']+8*GIB if model['repo']==FLUX_REPO else 0)
|
required_ram=max(20*GIB,encoder['size']*2+4*GIB,model['size']+8*GIB if image_adapter(model)['offload'] else 0)
|
||||||
headroom=cgroup_headroom()
|
headroom=cgroup_headroom()
|
||||||
if headroom is not None and headroom<required_ram:raise ValueError('Deck-RAM-Limit reicht für Textencoder und Arbeitsdaten nicht aus.')
|
if headroom is not None and headroom<required_ram:raise ValueError('Deck-RAM-Limit reicht für Textencoder und Arbeitsdaten nicht aus.')
|
||||||
if mem.get('MemAvailable',0)<required_ram+4*GIB:raise ValueError('Aktuell zu wenig freier System-RAM; produktive Dienste bleiben unverändert.')
|
if mem.get('MemAvailable',0)<required_ram+4*GIB:raise ValueError('Aktuell zu wenig freier System-RAM; produktive Dienste bleiben unverändert.')
|
||||||
gpu,encoder_gpu=select_gpus(probe(),model['size'],encoder['size'],vae['size'],offload=model['repo']==FLUX_REPO);self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
gpu,encoder_gpu=select_gpus(probe(),model['size'],encoder['size'],vae['size'],offload=image_adapter(model)['offload']);self.root.mkdir(parents=True,exist_ok=True,mode=0o700)
|
||||||
if shutil.disk_usage(self.root).free<10*GIB:raise ValueError('Weniger als 10 GiB freier Plattenspeicher.')
|
if shutil.disk_usage(self.root).free<10*GIB:raise ValueError('Weniger als 10 GiB freier Plattenspeicher.')
|
||||||
job_id=uuid.uuid4().hex;seed=params['seed'] if params['seed']>=0 else secrets.randbelow(2147483648)
|
job_id=uuid.uuid4().hex;seed=params['seed'] if params['seed']>=0 else secrets.randbelow(2147483648)
|
||||||
self.cancel.clear();self.job=dict(id=job_id,state='running',phase='Bildlaufzeit startet',profile_id=profile_id,profile_name=profile['name'],started_at=time.time(),gpu=gpu['name'],encoder_gpu=encoder_gpu['name'],seed=seed,parameters=params,reference_count=len(references))
|
self.cancel.clear();self.job=dict(id=job_id,state='running',phase='Bildlaufzeit startet',profile_id=profile_id,profile_name=profile['name'],started_at=time.time(),gpu=gpu['name'],encoder_gpu=encoder_gpu['name'],seed=seed,parameters=params,reference_count=len(references))
|
||||||
@@ -181,7 +181,7 @@ class ImageTests:
|
|||||||
if self.cancel.is_set():raise InterruptedError()
|
if self.cancel.is_set():raise InterruptedError()
|
||||||
self._phase('Prompt-Aufwerter entladen; Bildlaufzeit startet')
|
self._phase('Prompt-Aufwerter entladen; Bildlaufzeit startet')
|
||||||
directory.mkdir(mode=0o700)
|
directory.mkdir(mode=0o700)
|
||||||
for role,item,filename in [('unet',model,'model.safetensors' if model['repo']==FLUX_REPO else 'model.gguf'),('clip',encoder,'encoder.safetensors'),('vae',vae,'vae.safetensors')]:
|
for role,item,filename in [('unet',model,image_adapter(model)['model_filename']),('clip',encoder,'encoder.safetensors'),('vae',vae,'vae.safetensors')]:
|
||||||
dest=directory/'models'/role;dest.mkdir(parents=True);(dest/filename).symlink_to(self.model_path(item))
|
dest=directory/'models'/role;dest.mkdir(parents=True);(dest/filename).symlink_to(self.model_path(item))
|
||||||
for folder in ('output','temp','user','input'):(directory/folder).mkdir()
|
for folder in ('output','temp','user','input'):(directory/folder).mkdir()
|
||||||
names=[]
|
names=[]
|
||||||
@@ -218,7 +218,7 @@ class ImageTests:
|
|||||||
if time.monotonic()>deadline:raise ValueError('Bildlaufzeit wurde nicht rechtzeitig bereit.')
|
if time.monotonic()>deadline:raise ValueError('Bildlaufzeit wurde nicht rechtzeitig bereit.')
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
nodes=request('/object_info')
|
nodes=request('/object_info')
|
||||||
graph=workflow(prompt,params,seed,'flux' if model['repo']==FLUX_REPO else 'qwen',names)
|
graph=workflow(prompt,params,seed,image_adapter(model)['workflow'],names)
|
||||||
required={node['class_type'] for node in graph.values()}
|
required={node['class_type'] for node in graph.values()}
|
||||||
if not required.issubset(nodes):raise ValueError('Der installierten Bildlaufzeit fehlen erforderliche Nodes für dieses Bildmodell.')
|
if not required.issubset(nodes):raise ValueError('Der installierten Bildlaufzeit fehlen erforderliche Nodes für dieses Bildmodell.')
|
||||||
self._phase('Auftrag wird verarbeitet · Textencoder auf RTX 3060 · Bildberechnung auf RTX 5080')
|
self._phase('Auftrag wird verarbeitet · Textencoder auf RTX 3060 · Bildberechnung auf RTX 5080')
|
||||||
|
|||||||
+23
-9
@@ -59,14 +59,28 @@ FLUX_COMPONENTS={
|
|||||||
'text_encoder':{'label':'Textencoder für FLUX.2 Klein 9B','repo':FLUX_COMPONENT_REPO,'files':['split_files/text_encoders/qwen_3_8b_fp8mixed.safetensors']},
|
'text_encoder':{'label':'Textencoder für FLUX.2 Klein 9B','repo':FLUX_COMPONENT_REPO,'files':['split_files/text_encoders/qwen_3_8b_fp8mixed.safetensors']},
|
||||||
'vae':{'label':'VAE für FLUX.2','repo':FLUX_COMPONENT_REPO,'files':['split_files/vae/flux2-vae.safetensors']}
|
'vae':{'label':'VAE für FLUX.2','repo':FLUX_COMPONENT_REPO,'files':['split_files/vae/flux2-vae.safetensors']}
|
||||||
}
|
}
|
||||||
|
# One registry owns runtime, workflow, format, components and resource policy.
|
||||||
|
IMAGE_ADAPTERS={
|
||||||
|
'qwen_image_21':dict(label='Qwen Image 2.1',workflow='qwen',model_filename='model.gguf',suffix='.gguf',runtime='ComfyUI + ComfyUI-GGUF',reference_images=4,offload=False,components={k:{'repo':QWEN_REPO,**v} for k,v in QWEN_COMPONENTS.items()},sources={QWEN_REPO.lower(),'qwen/qwen-image-2.1','comfy-org/qwen-image-2.1'},architectures={'qwenimage21transformer2dmodel','qwen_image_2_1'}),
|
||||||
|
'flux2_klein_9b':dict(label='FLUX.2 Klein 9B',workflow='flux',model_filename='model.safetensors',suffix='.safetensors',runtime='ComfyUI',reference_images=4,offload=True,components=FLUX_COMPONENTS,sources={FLUX_REPO.lower(),'black-forest-labs/flux.2-klein-9b','comfy-org/flux2-klein-9b'},architectures={'flux2klein9btransformer2dmodel','flux2_klein_9b'}),
|
||||||
|
}
|
||||||
|
def image_adapter(model):
|
||||||
|
if not model:return None
|
||||||
|
filename=model.get('file','')
|
||||||
|
if not file_role(filename)['profile_eligible'] or '-of-' in filename:return None
|
||||||
|
meta=model.get('runtime_metadata') or {};architecture=str(meta.get('architecture') or '').lower()
|
||||||
|
lineage={str(x).lower() for x in meta.get('base_models',[]) if isinstance(x,str)}
|
||||||
|
repo=model.get('repo','').lower()
|
||||||
|
matches=[a for a in IMAGE_ADAPTERS.values() if filename.endswith(a['suffix']) and (repo in a['sources'] or architecture in a['architectures'] or bool(lineage & a['sources']))]
|
||||||
|
return matches[0] if len(matches)==1 else None
|
||||||
|
|
||||||
def image_recipe(model):
|
def image_recipe(model):
|
||||||
if model['repo']==QWEN_REPO and model['file'].endswith('.gguf'):return QWEN_COMPONENTS
|
adapter=image_adapter(model)
|
||||||
if model['repo']==FLUX_REPO and model['file']=='Flux.2 Klein-9B_fp16_nsfw.safetensors':return FLUX_COMPONENTS
|
return adapter['components'] if adapter else None
|
||||||
return None
|
|
||||||
|
|
||||||
def image_capabilities(model):
|
def image_capabilities(model):
|
||||||
"""All currently supported image recipes have a reference-image workflow."""
|
adapter=image_adapter(model)
|
||||||
return {'reference_images':4 if model and image_recipe(model) else 0}
|
return {'reference_images':adapter['reference_images'] if adapter else 0}
|
||||||
|
|
||||||
LTX_REPO='Lightricks/LTX-2.5'
|
LTX_REPO='Lightricks/LTX-2.5'
|
||||||
LTX_FILE='diffusion_models/ltx-2.5-22b-distilled-transformer-bf16.safetensors'
|
LTX_FILE='diffusion_models/ltx-2.5-22b-distilled-transformer-bf16.safetensors'
|
||||||
@@ -126,9 +140,9 @@ class Profiles:
|
|||||||
p['model']=self.catalog.entry(p['model_id'])
|
p['model']=self.catalog.entry(p['model_id'])
|
||||||
if p['kind']=='image':p['capabilities']=image_capabilities(p['model'])
|
if p['kind']=='image':p['capabilities']=image_capabilities(p['model'])
|
||||||
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
|
p['blockers']=['Für dieses Profil ist noch kein ausführbarer Worker angebunden.']
|
||||||
if p['kind']=='image' and (image_recipe(p['model']) or 'qwen-image-2.1' in p['model']['repo'].lower()):
|
if p['kind']=='image' and image_recipe(p['model']):
|
||||||
p['blockers']=['Eine eigene Bildlaufzeit muss eingerichtet werden; llama.cpp führt keine Bildmodelle aus.']
|
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']) or QWEN_COMPONENTS).items())):
|
for role,info in reversed(list(image_recipe(p['model']).items())):
|
||||||
try:self._component(p['model'],role,p.get('components',{}).get(role))
|
try:self._component(p['model'],role,p.get('components',{}).get(role))
|
||||||
except ValueError:p['blockers'].insert(0,info['label']+' fehlt oder ist noch nicht zugeordnet.')
|
except ValueError:p['blockers'].insert(0,info['label']+' fehlt oder ist noch nicht zugeordnet.')
|
||||||
except ValueError as exc:p['model']=None;p['blockers']=[str(exc)]
|
except ValueError as exc:p['model']=None;p['blockers']=[str(exc)]
|
||||||
@@ -245,8 +259,8 @@ class Profiles:
|
|||||||
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. Die YuE2-Musikanbindung verwendet diese Dateien über audio.cpp. Speichern startet kein Modell; anschließend Musik → Testen 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_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_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 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.')
|
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(),
|
return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime=image_adapter(model)['runtime'],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.')
|
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 image_adapter(model)['workflow']=='flux' 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):
|
def manual_components(self,model,repo=None):
|
||||||
source=self.catalog.files(repo or model['repo'],None if repo else model['revision'])
|
source=self.catalog.files(repo or model['repo'],None if repo else model['revision'])
|
||||||
entries=self.catalog.status()['entries'];requirements=[]
|
entries=self.catalog.status()['entries'];requirements=[]
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ class ImageTestTests(unittest.TestCase):
|
|||||||
self.assertEqual(image_capabilities(dict(repo=FLUX_REPO,file='Flux.2 Klein-9B_fp16_nsfw.safetensors'))['reference_images'],4)
|
self.assertEqual(image_capabilities(dict(repo=FLUX_REPO,file='Flux.2 Klein-9B_fp16_nsfw.safetensors'))['reference_images'],4)
|
||||||
self.assertEqual(image_capabilities(dict(repo=QWEN_REPO,file='model.gguf'))['reference_images'],4)
|
self.assertEqual(image_capabilities(dict(repo=QWEN_REPO,file='model.gguf'))['reference_images'],4)
|
||||||
self.assertEqual(image_capabilities(dict(repo='unknown',file='model.safetensors'))['reference_images'],0)
|
self.assertEqual(image_capabilities(dict(repo='unknown',file='model.safetensors'))['reference_images'],0)
|
||||||
|
def test_family_registry_reuses_workflow_from_lineage_not_filename(self):
|
||||||
|
from profiles import image_adapter,image_recipe,FLUX_COMPONENTS
|
||||||
|
m=dict(repo='someone/new-weights',file='custom.safetensors',runtime_metadata={'base_models':['black-forest-labs/FLUX.2-klein-9B']})
|
||||||
|
self.assertEqual(image_adapter(m)['workflow'],'flux')
|
||||||
|
self.assertEqual(image_recipe(m),FLUX_COMPONENTS)
|
||||||
|
m['runtime_metadata']={};m['file']='flux-qwen-looking-name.safetensors'
|
||||||
|
self.assertIsNone(image_adapter(m))
|
||||||
|
m['runtime_metadata']={'architecture':'Flux2Transformer2DModel'}
|
||||||
|
self.assertIsNone(image_adapter(m))
|
||||||
|
m['runtime_metadata']={'architecture':'qwen_image_2_1'};m['file']='new.gguf'
|
||||||
|
self.assertEqual(image_adapter(m)['workflow'],'qwen')
|
||||||
|
m['file']='vae/new.gguf';self.assertIsNone(image_adapter(m))
|
||||||
def test_reference_validation_enforces_profile_limit_and_signature(self):
|
def test_reference_validation_enforces_profile_limit_and_signature(self):
|
||||||
import base64
|
import base64
|
||||||
png='data:image/png;base64,'+base64.b64encode(b'\x89PNG\r\n\x1a\nsynthetic').decode()
|
png='data:image/png;base64,'+base64.b64encode(b'\x89PNG\r\n\x1a\nsynthetic').decode()
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class ManagementTests(unittest.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.tmp=tempfile.TemporaryDirectory();self.root=Path(self.tmp.name);self.model_id='a'*64
|
self.tmp=tempfile.TemporaryDirectory();self.root=Path(self.tmp.name);self.model_id='a'*64
|
||||||
target=self.root/'models'/self.model_id;target.mkdir(parents=True);(target/'model.gguf').write_bytes(b'GGUFtest')
|
target=self.root/'models'/self.model_id;target.mkdir(parents=True);(target/'model.gguf').write_bytes(b'GGUFtest')
|
||||||
self.entry=dict(repo='test/Qwen-Image-2.1',file='test.gguf',kind='image',size=8,revision='b'*40,sha256=hashlib.sha256(b'GGUFtest').hexdigest(),downloaded_at=1)
|
self.entry=dict(runtime_metadata={'architecture':'qwen_image_2_1'},repo='test/Qwen-Image-2.1',file='test.gguf',kind='image',size=8,revision='b'*40,sha256=hashlib.sha256(b'GGUFtest').hexdigest(),downloaded_at=1)
|
||||||
(target/'entry.json').write_text(json.dumps(self.entry));self.catalog=Catalog(self.root/'models');self.profiles=Profiles(self.root/'profiles.json',self.catalog)
|
(target/'entry.json').write_text(json.dumps(self.entry));self.catalog=Catalog(self.root/'models');self.profiles=Profiles(self.root/'profiles.json',self.catalog)
|
||||||
def tearDown(self):self.tmp.cleanup()
|
def tearDown(self):self.tmp.cleanup()
|
||||||
def request(self):return dict(id=None,revision=0,name='image-test',kind='image',model_id=self.model_id,parameters=dict(width=1024,height=1024,steps=25,seed=-1,guidance=1))
|
def request(self):return dict(id=None,revision=0,name='image-test',kind='image',model_id=self.model_id,parameters=dict(width=1024,height=1024,steps=25,seed=-1,guidance=1))
|
||||||
|
|||||||
Reference in New Issue
Block a user