Recognize model family hints and reusable runtimes
This commit is contained in:
@@ -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;
|
||||
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
|
||||
|
||||
Beim ersten Öffnen werden Oberflächenkennwort und separater API-Token eingerichtet.
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ window.CatalogUI = (() => {
|
||||
}
|
||||
function executionHTML(x) {
|
||||
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) {
|
||||
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
@@ -82,6 +82,17 @@ def capabilities(model):
|
||||
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')
|
||||
|
||||
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:
|
||||
def __init__(self,root):
|
||||
from hub_auth import HubAuth
|
||||
@@ -142,11 +153,11 @@ class Catalog:
|
||||
elif '/' in q:direct=repo_id(q)
|
||||
if 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'):
|
||||
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['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))
|
||||
if not rows and q and kind=='chat' and purpose=='model':
|
||||
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.'
|
||||
scanned=len(rows)
|
||||
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):
|
||||
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.")
|
||||
@@ -173,7 +184,7 @@ class Catalog:
|
||||
size=f.get('size',f.get('lfs',{}).get('size'))
|
||||
if not isinstance(size,int) or size<1:continue
|
||||
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:
|
||||
if len(self.cache)>=64:self.cache.pop(next(iter(self.cache)))
|
||||
self.cache[cache_key]=(time.monotonic(),result)
|
||||
@@ -240,7 +251,7 @@ class Catalog:
|
||||
if received!=item['size']:raise ValueError('Unvollständiger Download.')
|
||||
if item['sha256'] and digest.hexdigest()!=item['sha256']:raise ValueError('SHA-256-Prüfung fehlgeschlagen.')
|
||||
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')
|
||||
terminal='complete';error=None
|
||||
except Exception as exc:
|
||||
|
||||
+12
-3
@@ -6,29 +6,38 @@ from tts_runtime import REPO, REVISION
|
||||
|
||||
def assess(model, installed, profile=None):
|
||||
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']:
|
||||
result.update(state='component',label='Zusatzdatei',message='Diese Datei wird einem kompatiblen Modellprofil zugeordnet und benötigt keine eigene Laufzeit.')
|
||||
return result
|
||||
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)'])
|
||||
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()])
|
||||
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)'])
|
||||
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.')
|
||||
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':
|
||||
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':
|
||||
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):
|
||||
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.')
|
||||
if not result['runtime']:return result
|
||||
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.')
|
||||
if not result['installed']:
|
||||
if not result['installed'] and result['state']!='workflow_missing':
|
||||
result.update(state='runtime_missing',label='Passende Laufzeit fehlt')
|
||||
if profile:
|
||||
result['blockers']=profile.get('blockers',[])
|
||||
|
||||
@@ -315,7 +315,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
return self.respond(overview(data['files'],self.server.hardware.snapshot()))
|
||||
if path.path=='/api/v1/catalog/files':
|
||||
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)
|
||||
if path.path=='/api/v1/catalog':
|
||||
data=self.server.catalog.status()
|
||||
|
||||
+6
-1
@@ -4,9 +4,14 @@ import tempfile
|
||||
import time
|
||||
import unittest
|
||||
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):
|
||||
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):
|
||||
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)
|
||||
|
||||
@@ -25,3 +25,11 @@ class ExecutionSetupTests(unittest.TestCase):
|
||||
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=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')
|
||||
|
||||
Reference in New Issue
Block a user