diff --git a/README.md b/README.md
index d2d86fc..4fac0ec 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/catalog-ui.js b/catalog-ui.js
index 72808eb..ea879a4 100644
--- a/catalog-ui.js
+++ b/catalog-ui.js
@@ -9,7 +9,7 @@ window.CatalogUI = (() => {
}
function executionHTML(x) {
if(!x)return '
Ausführbarkeit noch nicht geprüft.
';
- return `${escape(x.label)}${x.runtime?`
${escape(x.runtime)} · ${x.installed?'bereits installiert – wird wiederverwendet':'noch nicht eingerichtet'}
`:''}
${escape(x.message)}
${x.components?.length?`
Benötigt: ${x.components.map(escape).join(' · ')}. Bei Profilen unter „Komponenten“ zuordnen; TTS enthält das komplette Paket.
Installierte Laufzeit bedeutet nicht, dass Modell und Kontext in den Speicher passen. Ein Test startet nur auf deinen Auftrag.
`;
}
function html(library, downloads=false) {
return `
HUGGING FACE · LIVE-KATALOG
${downloads ? 'Downloads' : library ? 'Deine Dateien auf Athena' : 'Das nächste Modell entdecken'}
${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.'}
diff --git a/catalog.py b/catalog.py
index 6d30ff7..89008eb 100644
--- a/catalog.py
+++ b/catalog.py
@@ -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=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:
diff --git a/execution_setup.py b/execution_setup.py
index 8a8c356..5608f7c 100644
--- a/execution_setup.py
+++ b/execution_setup.py
@@ -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',[])
diff --git a/server.py b/server.py
index bc9800a..1c17da4 100644
--- a/server.py
+++ b/server.py
@@ -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()
diff --git a/test_catalog.py b/test_catalog.py
index ea40486..20173a9 100644
--- a/test_catalog.py
+++ b/test_catalog.py
@@ -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)
diff --git a/test_execution_setup.py b/test_execution_setup.py
index 24f8319..ae086bb 100644
--- a/test_execution_setup.py
+++ b/test_execution_setup.py
@@ -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')