Recognize model family hints and reusable runtimes

This commit is contained in:
Mikei386
2026-09-29 20:24:35 +02:00
parent f1ccb02e10
commit 3dcf2d3f76
7 changed files with 50 additions and 11 deletions
+6
View File
@@ -23,6 +23,12 @@ Der Bildtest und `/v1/images/edits` akzeptieren beim geprüften Qwen-Image-2.1-R
bis zu vier Referenzbilder. Die Zahl wird pro aktivem Modellrezept angezeigt; bis zu vier Referenzbilder. Die Zahl wird pro aktivem Modellrezept angezeigt;
andere Bildmodelle erhalten keine ungeprüfte Edit-Fähigkeit. andere Bildmodelle erhalten keine ungeprüfte Edit-Fähigkeit.
Entdecken und Bibliothek zeigen die aus Hugging-Face-Metadaten erkennbare
Modellfamilie und den Status der vorhandenen Laufzeit. Bei unbekannten
Bildvarianten kann Deck eine vorhandene ComfyUI-Installation als mögliche Basis
anzeigen. Ohne geprüftes Komponentenrezept und Workflow bleibt das Modell
gesperrt; ein weiterer Runtime-Build wird daraus nicht automatisch abgeleitet.
## Zugang und API-Token ## Zugang und API-Token
Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet. Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet.
+1 -1
View File
@@ -9,7 +9,7 @@ window.CatalogUI = (() => {
} }
function executionHTML(x) { function executionHTML(x) {
if(!x)return '<p>Ausführbarkeit noch nicht geprüft.</p>'; if(!x)return '<p>Ausführbarkeit noch nicht geprüft.</p>';
return `<section class="note execution-setup"><strong>${escape(x.label)}</strong>${x.runtime?`<p>${escape(x.runtime)} · ${x.installed?'bereits installiert – wird wiederverwendet':'noch nicht eingerichtet'}</p>`:''}<p>${escape(x.message)}</p>${x.components?.length?`<p>Benötigt: ${x.components.map(escape).join(' · ')}. Bei Profilen unter „Komponenten“ zuordnen; TTS enthält das komplette Paket.</p>`:''}${x.route?`<a class="link" href="${escape(x.route)}">${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →</a>`:''}${x.alternative?`<p><a class="link" href="${escape(x.alternative.route)}">${escape(x.alternative.label)} →</a></p>`:''}<p class="small">Installierte Laufzeit bedeutet nicht, dass Modell und Kontext in den Speicher passen. Ein Test startet nur auf deinen Auftrag.</p></section>`; return `<section class="note execution-setup"><strong>${escape(x.label)}</strong><p>Modellfamilie: ${escape(x.family||'Unbekannt')}${x.evidence?' · '+escape(x.evidence):''}</p>${x.runtime?`<p>${escape(x.runtime)} · ${x.installed?'bereits installiert – wird wiederverwendet':'noch nicht eingerichtet'}</p>`:''}<p>${escape(x.message)}</p>${x.components?.length?`<p>Benötigt: ${x.components.map(escape).join(' · ')}. Bei Profilen unter „Komponenten“ zuordnen; TTS enthält das komplette Paket.</p>`:''}${x.route?`<a class="link" href="${escape(x.route)}">${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →</a>`:''}${x.alternative?`<p><a class="link" href="${escape(x.alternative.route)}">${escape(x.alternative.label)} →</a></p>`:''}<p class="small">Installierte Laufzeit bedeutet nicht, dass Modell und Kontext in den Speicher passen. Ein Test startet nur auf deinen Auftrag.</p></section>`;
} }
function html(library, downloads=false) { function html(library, downloads=false) {
return `<section id="live-catalog"><div class="section-heading"><div><span class="kicker">HUGGING FACE · LIVE-KATALOG</span><h2>${downloads ? 'Downloads' : library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}</h2><p>${downloads ? 'Abgeschlossene Einträge ausblenden. Die Dateien bleiben in deiner Bibliothek.' : library ? 'Heruntergeladene Dateien mit festgehaltener Version und Integritätsprüfung.' : 'Modell suchen, Variante vergleichen und gezielt auf Athena herunterladen.'}</p></div><button class="secondary" id="hub-refresh">Aktualisieren</button></div> return `<section id="live-catalog"><div class="section-heading"><div><span class="kicker">HUGGING FACE · LIVE-KATALOG</span><h2>${downloads ? 'Downloads' : library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}</h2><p>${downloads ? 'Abgeschlossene Einträge ausblenden. Die Dateien bleiben in deiner Bibliothek.' : library ? 'Heruntergeladene Dateien mit festgehaltener Version und Integritätsprüfung.' : 'Modell suchen, Variante vergleichen und gezielt auf Athena herunterladen.'}</p></div><button class="secondary" id="hub-refresh">Aktualisieren</button></div>
+16 -5
View File
@@ -82,6 +82,17 @@ def capabilities(model):
tools=bool(tags & {'tool-use','tool-calling','function-calling','function_calling'}) tools=bool(tags & {'tool-use','tool-calling','function-calling','function_calling'})
return dict(vision='Hinweis laut Hub' if vision else 'Projektordatei vorhanden' if files else 'Unbekannt',tools='Hinweis laut Hub' if tools else 'Unbekannt') return dict(vision='Hinweis laut Hub' if vision else 'Projektordatei vorhanden' if files else 'Unbekannt',tools='Hinweis laut Hub' if tools else 'Unbekannt')
def runtime_metadata(model):
"""Persist only bounded, non-secret Hub hints; never treat them as a runnable recipe."""
config=model.get('config') if isinstance(model.get('config'),dict) else {}
architectures=config.get('architectures') if isinstance(config.get('architectures'),list) else []
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)
tags=[x for x in model.get('tags',[]) if isinstance(x,str) and len(x)<=80][:32] if isinstance(model.get('tags'),list) else []
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,
architecture=architecture,tags=tags)
class Catalog: class Catalog:
def __init__(self,root): def __init__(self,root):
from hub_auth import HubAuth from hub_auth import HubAuth
@@ -142,11 +153,11 @@ class Catalog:
elif '/' in q:direct=repo_id(q) elif '/' in q:direct=repo_id(q)
if direct: if direct:
item=self._metadata('/api/models/'+direct) item=self._metadata('/api/models/'+direct)
return dict(models=[dict(repo=item['id'],downloads=item.get('downloads'),gated=item.get('gated',False),created_at=item.get('createdAt'),capabilities=capabilities(item))],base_only=base_only,scanned=1,notice='Direkt ausgewähltes Repository: Kategorie- und Basismodellfilter werden für diesen Treffer nicht angewendet.') return dict(models=[dict(repo=item['id'],downloads=item.get('downloads'),gated=item.get('gated',False),created_at=item.get('createdAt'),capabilities=capabilities(item),runtime_metadata=runtime_metadata(item))],base_only=base_only,scanned=1,notice='Direkt ausgewähltes Repository: Kategorie- und Basismodellfilter werden für diesen Treffer nicht angewendet.')
if q.lower().endswith('.gguf'): if q.lower().endswith('.gguf'):
q=q[:-5];notice='GGUF-Dateiname als Repository-Suchbegriff verwendet. Die Datei wählst du anschließend in den Modelldetails.' q=q[:-5];notice='GGUF-Dateiname als Repository-Suchbegriff verwendet. Die Datei wählst du anschließend in den Modelldetails.'
query=dict(search=q,filter='gguf' if purpose=='projector' else KINDS[kind],limit=100 if base_only else 20,sort='createdAt' if sort=='newest' else 'downloads',direction=-1) query=dict(search=q,filter='gguf' if purpose=='projector' else KINDS[kind],limit=100 if base_only else 20,sort='createdAt' if sort=='newest' else 'downloads',direction=-1)
query['expand']=['downloads','createdAt','gated','cardData','tags','pipeline_tag'] query['expand']=['downloads','createdAt','gated','cardData','tags','pipeline_tag','library_name','config']
rows=self._metadata('/api/models?'+urllib.parse.urlencode(query,doseq=True)) rows=self._metadata('/api/models?'+urllib.parse.urlencode(query,doseq=True))
if not rows and q and kind=='chat' and purpose=='model': if not rows and q and kind=='chat' and purpose=='model':
fallback=dict(query,filter='gguf',expand=['downloads','createdAt','gated','cardData','tags','pipeline_tag']) fallback=dict(query,filter='gguf',expand=['downloads','createdAt','gated','cardData','tags','pipeline_tag'])
@@ -155,7 +166,7 @@ class Catalog:
if rows:notice='GGUF-Sprachmodelle ohne reguläres Kategorie-Tag gefunden. Bei aktiver Basismodellauswahl bleiben Quantisierungen ausgeblendet.' if rows:notice='GGUF-Sprachmodelle ohne reguläres Kategorie-Tag gefunden. Bei aktiver Basismodellauswahl bleiben Quantisierungen ausgeblendet.'
scanned=len(rows) scanned=len(rows)
if base_only:rows=[x for x in rows if not is_derived_model(x)] if base_only:rows=[x for x in rows if not is_derived_model(x)]
return {'models':[dict(repo=x['id'],downloads=x.get('downloads'),gated=x.get('gated',False),created_at=x.get('createdAt'),capabilities=capabilities(x)) for x in rows[:20]],'base_only':base_only,'scanned':scanned,'notice':notice} return {'models':[dict(repo=x['id'],downloads=x.get('downloads'),gated=x.get('gated',False),created_at=x.get('createdAt'),capabilities=capabilities(x),runtime_metadata=runtime_metadata(x)) for x in rows[:20]],'base_only':base_only,'scanned':scanned,'notice':notice}
def files(self,repo,revision=None): def files(self,repo,revision=None):
repo=repo_id(repo) repo=repo_id(repo)
if revision is not None and (not isinstance(revision,str) or not re.fullmatch("[a-f0-9]{40}",revision)):raise ValueError("Ungültige Quellversion.") if revision is not None and (not isinstance(revision,str) or not re.fullmatch("[a-f0-9]{40}",revision)):raise ValueError("Ungültige Quellversion.")
@@ -173,7 +184,7 @@ class Catalog:
size=f.get('size',f.get('lfs',{}).get('size')) size=f.get('size',f.get('lfs',{}).get('size'))
if not isinstance(size,int) or size<1:continue if not isinstance(size,int) or size<1:continue
files.append(dict(name=name,size=size,sha256=f.get('lfs',{}).get('sha256'))) files.append(dict(name=name,size=size,sha256=f.get('lfs',{}).get('sha256')))
result=dict(auth_configured=self.hub_auth.status()['configured'],capabilities=capabilities(data),repo=repo,revision=revision,files=files,gated=data.get('gated',False),license=(data.get('cardData') or {}).get('license'),url='https://huggingface.co/'+repo) result=dict(auth_configured=self.hub_auth.status()['configured'],capabilities=capabilities(data),runtime_metadata=runtime_metadata(data),repo=repo,revision=revision,files=files,gated=data.get('gated',False),license=(data.get('cardData') or {}).get('license'),url='https://huggingface.co/'+repo)
with self.cache_lock: with self.cache_lock:
if len(self.cache)>=64:self.cache.pop(next(iter(self.cache))) if len(self.cache)>=64:self.cache.pop(next(iter(self.cache)))
self.cache[cache_key]=(time.monotonic(),result) self.cache[cache_key]=(time.monotonic(),result)
@@ -240,7 +251,7 @@ class Catalog:
if received!=item['size']:raise ValueError('Unvollständiger Download.') if received!=item['size']:raise ValueError('Unvollständiger Download.')
if item['sha256'] and digest.hexdigest()!=item['sha256']:raise ValueError('SHA-256-Prüfung fehlgeschlagen.') if item['sha256'] and digest.hexdigest()!=item['sha256']:raise ValueError('SHA-256-Prüfung fehlgeschlagen.')
partial.replace(dest) partial.replace(dest)
entry=dict(repo=data['repo'],revision=data['revision'],file=item['name'],size=received,sha256=digest.hexdigest(),upstream_hash_verified=bool(item['sha256']),kind=kind,downloaded_at=time.time(),state='downloaded',runtime_ready=False) entry=dict(repo=data['repo'],revision=data['revision'],file=item['name'],size=received,sha256=digest.hexdigest(),upstream_hash_verified=bool(item['sha256']),kind=kind,downloaded_at=time.time(),state='downloaded',runtime_ready=False,runtime_metadata=data.get('runtime_metadata',{}))
temp=target/'entry.tmp';temp.write_text(json.dumps(entry));temp.replace(target/'entry.json') temp=target/'entry.tmp';temp.write_text(json.dumps(entry));temp.replace(target/'entry.json')
terminal='complete';error=None terminal='complete';error=None
except Exception as exc: except Exception as exc:
+12 -3
View File
@@ -6,29 +6,38 @@ from tts_runtime import REPO, REVISION
def assess(model, installed, profile=None): def assess(model, installed, profile=None):
kind=model.get('kind'); filename=model.get('file',''); repo=model.get('repo','') kind=model.get('kind'); filename=model.get('file',''); repo=model.get('repo','')
result=dict(state='unsupported',label='Noch nicht unterstützt',runtime=None,installed=False,route=None,components=[],message='Für dieses Modell ist in Deck noch kein geprüfter Ausführungsweg hinterlegt. Herunterladen und Aufbewahren ist möglich; eine Installation allein genügt nicht.') meta=model.get('runtime_metadata') if isinstance(model.get('runtime_metadata'),dict) else {}
architecture=meta.get('architecture') if isinstance(meta.get('architecture'),str) else None
result=dict(state='unsupported',label='Noch nicht unterstützt',runtime=None,installed=False,route=None,components=[],family=architecture or 'Unbekannt',evidence='Hub-Metadaten' if architecture else 'Keine ausreichenden Metadaten',message='Für dieses Modell ist in Deck noch kein geprüfter Ausführungsweg hinterlegt. Herunterladen und Aufbewahren ist möglich; eine Installation allein genügt nicht.')
if not file_role(filename)['profile_eligible']: if not file_role(filename)['profile_eligible']:
result.update(state='component',label='Zusatzdatei',message='Diese Datei wird einem kompatiblen Modellprofil zugeordnet und benötigt keine eigene Laufzeit.') result.update(state='component',label='Zusatzdatei',message='Diese Datei wird einem kompatiblen Modellprofil zugeordnet und benötigt keine eigene Laufzeit.')
return result return result
if kind=='stt' and stt_supported(model): if kind=='stt' and stt_supported(model):
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.update(runtime='ComfyUI + ComfyUI-GGUF',route='#image-runtime',components=[x['label'] for x in image_recipe(model).values()]) result.update(runtime='ComfyUI + ComfyUI-GGUF',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.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)'])
elif kind=='chat' and filename.lower().endswith('.gguf'): elif kind=='chat' and filename.lower().endswith('.gguf'):
result.update(runtime='llama.cpp',route='#runtime',state='check',label='Kompatibilität beim Laden prüfen',message='llama.cpp ist der mögliche Ausführungsweg für dieses Chat-GGUF. Architektur und gewählte Optionen müssen vom installierten Build unterstützt werden. Der Testchat prüft die tatsächliche Ausführung.') result.update(runtime='llama.cpp',route='#runtime',state='check',label='Kompatibilität beim Laden prüfen',message='llama.cpp ist der mögliche Ausführungsweg für dieses Chat-GGUF. Architektur und gewählte Optionen müssen vom installierten Build unterstützt werden. Der Testchat prüft die tatsächliche Ausführung.')
elif kind=='image' and filename.lower().endswith(('.safetensors','.gguf')) and (meta.get('pipeline_tag') in ('text-to-image','image-to-image') or meta.get('library_name') in ('diffusers','comfyui')):
result.update(state='workflow_missing',label='Bildfamilie erkannt · Workflow fehlt',runtime='ComfyUI (vorhandene Bildlaufzeit prüfen)',installed=bool(installed.get('image')),route='#image-runtime',message='Hub-Metadaten ordnen die Datei einer Bildpipeline zu. Deck hat bereits eine gemeinsame Bildlaufzeit; ein neuer Build ist nicht automatisch nötig. Für diese Modellvariante fehlen noch ein geprüfter ComfyUI-Workflow und die genaue Komponentenliste. Bis dahin kann sie nicht gestartet werden.')
if not architecture:result['family']='Bildpipeline laut Hugging Face'
elif kind=='stt': elif kind=='stt':
result.update(message='Für diese STT-Datei fehlt die Anbindung. Die geprüfte llama.cpp-Variante von Qwen3-ASR kann unter Einrichten zusätzlich geladen und ausdrücklich einem Profil zugeordnet werden.',alternative=dict(route='#stt-runtime',label='Unterstützte STT-Alternative einrichten (Modellwechsel)')) result.update(message='Für diese STT-Datei fehlt die Anbindung. Die geprüfte llama.cpp-Variante von Qwen3-ASR kann unter Einrichten zusätzlich geladen und ausdrücklich einem Profil zugeordnet werden.',alternative=dict(route='#stt-runtime',label='Unterstützte STT-Alternative einrichten (Modellwechsel)'))
elif kind=='audio': elif kind=='audio':
result.update(message='Dieses TTS-Modell wird noch nicht ausgeführt. Deck unterstützt derzeit die festgelegte Qwen3-TTS-CustomVoice-Variante mit eingebauten Stimmen. MLX, Base mit Referenzstimme und andere TTS-Architekturen benötigen andere Anbindungen.',alternative=dict(route='#tts-runtime',label='Unterstützte TTS-Alternative ansehen (Modellwechsel)')) result.update(message='Dieses TTS-Modell wird noch nicht ausgeführt. Deck unterstützt derzeit die festgelegte Qwen3-TTS-CustomVoice-Variante mit eingebauten Stimmen. MLX, Base mit Referenzstimme und andere TTS-Architekturen benötigen andere Anbindungen.',alternative=dict(route='#tts-runtime',label='Unterstützte TTS-Alternative ansehen (Modellwechsel)'))
if video_recipe(model): if video_recipe(model):
result['family']='LTX 2.5'
result.update(runtime='LTX-Pipelines · Distilled BF16',state='runtime_missing',label='Komponenten und Video-Laufzeit einrichten',route='#video-runtime',components=[x['label'] for x in video_recipe(model).values()],message='Videoprofil anlegen und dort Komponenten öffnen: passende Dateien aus der Version des Hauptmodells nachladen und zuordnen. LTX-Laufzeit unter Einstellungen → Video installieren; danach Videoprofil in der Übersicht auswählen.') result.update(runtime='LTX-Pipelines · Distilled BF16',state='runtime_missing',label='Komponenten und Video-Laufzeit einrichten',route='#video-runtime',components=[x['label'] for x in video_recipe(model).values()],message='Videoprofil anlegen und dort Komponenten öffnen: passende Dateien aus der Version des Hauptmodells nachladen und zuordnen. LTX-Laufzeit unter Einstellungen → Video installieren; danach Videoprofil in der Übersicht auswählen.')
if not result['runtime']:return result if not result['runtime']:return result
result['installed']=bool(installed.get(kind)) result['installed']=bool(installed.get(kind))
if kind!='chat': if kind!='chat' and result['state']!='workflow_missing':
result.update(state='setup',label='Unterstützt – Einrichtung prüfen',message='Die Laufzeit wird für kompatible Modelle gemeinsam genutzt. Modellabhängige Komponenten werden dem jeweiligen Profil zugeordnet.') result.update(state='setup',label='Unterstützt – Einrichtung prüfen',message='Die Laufzeit wird für kompatible Modelle gemeinsam genutzt. Modellabhängige Komponenten werden dem jeweiligen Profil zugeordnet.')
if not result['installed']: if not result['installed'] and result['state']!='workflow_missing':
result.update(state='runtime_missing',label='Passende Laufzeit fehlt') result.update(state='runtime_missing',label='Passende Laufzeit fehlt')
if profile: if profile:
result['blockers']=profile.get('blockers',[]) result['blockers']=profile.get('blockers',[])
+1 -1
View File
@@ -315,7 +315,7 @@ class Handler(BaseHTTPRequestHandler):
return self.respond(overview(data['files'],self.server.hardware.snapshot())) return self.respond(overview(data['files'],self.server.hardware.snapshot()))
if path.path=='/api/v1/catalog/files': if path.path=='/api/v1/catalog/files':
data=dict(self.server.catalog.files(q.get('repo',[''])[0]));hardware=self.server.hardware.snapshot() data=dict(self.server.catalog.files(q.get('repo',[''])[0]));hardware=self.server.hardware.snapshot()
data['files']=[dict(f,assessment=assess(f['size'],hardware,f['name']),execution=self.execution_setup(dict(repo=data['repo'],revision=data['revision'],file=f['name'],kind=q.get('kind',['chat'])[0]))) for f in data['files']] data['files']=[dict(f,assessment=assess(f['size'],hardware,f['name']),execution=self.execution_setup(dict(repo=data['repo'],revision=data['revision'],file=f['name'],kind=q.get('kind',['chat'])[0],runtime_metadata=data.get('runtime_metadata',{})))) for f in data['files']]
return self.respond(data) return self.respond(data)
if path.path=='/api/v1/catalog': if path.path=='/api/v1/catalog':
data=self.server.catalog.status() data=self.server.catalog.status()
+6 -1
View File
@@ -4,9 +4,14 @@ import tempfile
import time import time
import unittest import unittest
from unittest.mock import patch from unittest.mock import patch
from catalog import Catalog, safe_url, repo_id, is_derived_model from catalog import Catalog, safe_url, repo_id, is_derived_model, runtime_metadata
class CatalogTests(unittest.TestCase): class CatalogTests(unittest.TestCase):
def test_bounded_hub_runtime_metadata(self):
hint=runtime_metadata(dict(pipeline_tag='text-to-image',library_name='diffusers',config={'architectures':['NewImagePipeline']},tags=['image-to-image']))
self.assertEqual(hint['architecture'],'NewImagePipeline')
self.assertEqual(hint['library_name'],'diffusers')
self.assertEqual(runtime_metadata(dict(config={'architectures':['x'*200]}))['architecture'],None)
def test_reject_untrusted_urls_and_repos(self): def test_reject_untrusted_urls_and_repos(self):
for value in ['http://huggingface.co/a','https://127.0.0.1/x','https://huggingface.co.evil.org/a','https://user@huggingface.co/a']: for value in ['http://huggingface.co/a','https://127.0.0.1/x','https://huggingface.co.evil.org/a','https://user@huggingface.co/a']:
with self.assertRaises(ValueError):safe_url(value) with self.assertRaises(ValueError):safe_url(value)
+8
View File
@@ -25,3 +25,11 @@ class ExecutionSetupTests(unittest.TestCase):
model=dict(kind='image',repo=QWEN_REPO,file='model.gguf') model=dict(kind='image',repo=QWEN_REPO,file='model.gguf')
self.assertEqual(assess(model,{'image':True},dict(runnable=False,blockers=['VAE fehlt']))['state'],'profile_incomplete') self.assertEqual(assess(model,{'image':True},dict(runnable=False,blockers=['VAE fehlt']))['state'],'profile_incomplete')
self.assertEqual(assess(model,{'image':True},dict(runnable=True))['state'],'ready') self.assertEqual(assess(model,{'image':True},dict(runnable=True))['state'],'ready')
def test_hub_image_family_reuses_installed_runtime_without_claiming_execution(self):
model=dict(kind='image',repo='other/new-image',file='weights.safetensors',runtime_metadata=dict(pipeline_tag='text-to-image',library_name='diffusers',architecture='NewImagePipeline'))
result=assess(model,{'image':True})
self.assertEqual(result['state'],'workflow_missing')
self.assertTrue(result['installed'])
self.assertEqual(result['family'],'NewImagePipeline')
self.assertIn('Workflow',result['message'])
self.assertEqual(assess(dict(model,kind='audio'),{'image':True})['state'],'unsupported')