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.

`:''}${x.route?`${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →`:''}${x.alternative?`

${escape(x.alternative.label)} →

`:''}

Installierte Laufzeit bedeutet nicht, dass Modell und Kontext in den Speicher passen. Ein Test startet nur auf deinen Auftrag.

`; + return `
${escape(x.label)}

Modellfamilie: ${escape(x.family||'Unbekannt')}${x.evidence?' · '+escape(x.evidence):''}

${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.

`:''}${x.route?`${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →`:''}${x.alternative?`

${escape(x.alternative.label)} →

`:''}

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')