${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(' · ')}. Modellabhängig zuordnen; benötigte Zusatzdateien gehören zum vollständigen Paket.
`:''}${x.suggested_components?.length?`
Zusatzdateien in dieser Quelle (Kompatibilität ungeprüft): ${x.suggested_components.map(escape).join(' · ')}. Nach dem Download unter Profil → Einrichtung & Komponenten prüfen.
${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(' · ')}. Modellabhängig zuordnen; benötigte Zusatzdateien gehören zum vollständigen Paket.
`:''}${x.suggested_components?.length?`
Zusatzdateien in dieser Quelle (Kompatibilität ungeprüft): ${x.suggested_components.map(escape).join(' · ')}. Nach dem Download unter Profil → Einrichtung & Komponenten prüfen.
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/execution_setup.py b/execution_setup.py
index 41aa6ee..0d3181e 100644
--- a/execution_setup.py
+++ b/execution_setup.py
@@ -50,7 +50,7 @@ def _assess(model, installed, profile=None):
return result
-def assess(model, installed, profile=None):
+def _assess_candidates(model, installed, profile=None):
result=_assess(model,installed,profile)
result['candidates']=[]
if result['state']=='component':return result
@@ -91,3 +91,17 @@ def assess(model, installed, profile=None):
add('category',label,key,route,'Option für diese Kategorie; Kompatibilität mit dieser unbekannten Modellfamilie NICHT bestätigt')
result['candidate_message']='Keine passende Laufzeit zuverlässig ermittelt. Modellkarte und Architektur müssen geprüft werden; keine automatische Installation auf Verdacht.'
return result
+
+
+def assess(model, installed, profile=None):
+ result=_assess_candidates(model,installed,profile)
+ if result['state']=='component':return result
+ verified=result['state'] not in ('unsupported','workflow_missing')
+ runtime=(next((c for c in result['candidates'] if c['id'] in ('voxcpm','audio_cpp')),None) or next((c for c in result['candidates'] if c['id']!='category'),None)) if verified else None
+ worker_missing=result['state'] in ('adapter_missing','workflow_missing','unsupported')
+ result['setup_steps']=[
+ dict(label='Modell erkennen',state='complete' if verified else 'blocked',message='Modellfamilie erkannt' if verified else 'Noch kein geprüfter Einrichtungsweg'),
+ dict(label='Laufzeit bereitstellen',state=('complete' if runtime['installed'] else 'action') if runtime else 'blocked',message=('Vorhandene Laufzeit wird wiederverwendet' if runtime['installed'] else 'Passende Laufzeit einrichten') if runtime else 'Keine automatische Installation auf Verdacht',route=runtime['route'] if runtime else None),
+ dict(label='Zusatzdateien zuordnen',state='complete' if result['state']=='ready' or not result['components'] and verified else 'action' if verified else 'blocked',message='Unter Einrichtung & Komponenten: vorhandene Dateien verwenden, fehlende herunterladen' if result['components'] else 'Keine Pflichtdateien im geprüften Rezept' if verified else 'Komponentenbedarf noch nicht zuverlässig bekannt'),
+ dict(label='Ausführung prüfen',state='blocked' if worker_missing else 'complete' if result['state']=='ready' else 'action',message='Generierungsanbindung fehlt noch; Installation allein reicht nicht' if worker_missing else 'Profil bereit' if result['state']=='ready' else 'Nach Einrichtung im Testbereich prüfen')]
+ return result
diff --git a/profiles-ui.js b/profiles-ui.js
index 3a9f773..79c312c 100644
--- a/profiles-ui.js
+++ b/profiles-ui.js
@@ -37,11 +37,11 @@ window.ProfilesUI=(()=>{
el('profile-editor').innerHTML='
Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}
${!data.manual?'':'
Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.
'}`:''}`;
const missing=data.requirements?.filter(r=>!r.available.length&&!r.optional)||[];
if(el('components-download-all')){el('components-download-all').disabled=!missing.length;el('components-download-all').onclick=async()=>{
const b=el('components-download-all');b.disabled=true;let count=0;
- try{const plan=missing.map(r=>{const f=r.candidates.find(f=>!f.gated||f.auth_configured);if(!f)throw Error('Downloadplan unvollständig: '+r.label+'. Quelle oder HF-Zugang prüfen. Es wurde noch kein Download gestartet.');return f;});for(const f of plan){const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';}
+ try{const plan=missing.map(r=>{const choices=r.candidates.filter(f=>!f.gated||f.auth_configured);if(choices.length>1)throw Error('Bitte Variante für '+r.label+' einzeln auswählen. Es wurde noch kein Download gestartet.');const f=choices[0];if(!f)throw Error('Downloadplan unvollständig: '+r.label+'. Quelle oder HF-Zugang prüfen. Es wurde noch kein Download gestartet.');return f;});for(const f of plan){const catalog=await api('catalog');if((catalog.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===f.repo&&j.file===f.name&&j.revision===f.revision))continue;await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});count++;}if(el('components-message'))el('components-message').textContent='Dateien sind im Downloadplan. Nach Abschluss Bibliothek aktualisieren und Zuordnung speichern; passende Dateien werden vorausgewählt.';}
catch(error){if(el('components-message'))el('components-message').textContent=(count?count+' Datei(en) bereits im Downloadplan. ':'')+error.message;b.disabled=false;}
};}
el('component-source-open').onclick=()=>{const value=el('component-source').value.trim();if(value)components(profile,value);};
diff --git a/profiles.py b/profiles.py
index c330104..41ead60 100644
--- a/profiles.py
+++ b/profiles.py
@@ -278,10 +278,10 @@ class Profiles:
candidates=[dict(f,repo=source.get('repo',info.get('repo',model['repo'])),revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for name in info['files'] for f in source['files'] if f['name']==name]
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
if voice_recipe(model):return dict(supported=True,requirements=requirements,source='https://huggingface.co/'+model['repo']+'/blob/'+model['revision']+'/README.md',license=source['license'],runtime='VoxCPM · eigene Python/PyTorch-Laufzeit',runtime_installed=bool(getattr(self,'voxcpm_runtime_ready',lambda:False)()),runtime_route='#voxcpm-runtime',worker_available=False,message='VoxCPM2 benötigt genau diese fünf Zusatzdateien derselben Quellversion. Ein separater Textencoder und ein Vision-Projektor werden nicht benötigt. tokenizer.json und tokenizer_config.json gehören zusammen, sie sind keine doppelten Modelle. Vorhandene Dateien werden wiederverwendet. Die passende Laufzeit kann unter Einstellungen → Laufzeiten → VoxCPM installiert werden. Der Deck-Worker fehlt noch; ein vollständiges Dateipaket allein kann noch nicht gestartet werden.')
- 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 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_route='#audio-cpp-runtime',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_route='#image-runtime',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.')
- return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime=image_adapter(model)['runtime'],runtime_installed=self.image_runtime_ready(),
+ return dict(supported=True,requirements=requirements,source=source['url'],license=source['license'],runtime=image_adapter(model)['runtime'],runtime_route='#image-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 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):
source=self.catalog.files(repo or model['repo'],None if repo else model['revision'])
diff --git a/test_execution_setup.py b/test_execution_setup.py
index 7c93bba..d43f5f2 100644
--- a/test_execution_setup.py
+++ b/test_execution_setup.py
@@ -51,3 +51,14 @@ class ExecutionSetupTests(unittest.TestCase):
result=self.check('music','audio-cpp/Yue2-3B-GGUF','yue2-vae-f16.gguf')
self.assertEqual(result['state'],'component')
self.assertEqual(result['candidates'],[])
+
+ def test_shared_setup_steps_reuse_runtime_and_never_claim_missing_worker(self):
+ for model,installed in [(dict(kind='image',repo=QWEN_REPO,file='model.gguf'),{'image':True}),(dict(kind='voice',repo='openbmb/VoxCPM2',file='model.safetensors'),{'voxcpm':True})]:
+ result=assess(model,installed)
+ self.assertEqual(result['setup_steps'][1]['state'],'complete')
+ self.assertIn('wiederverwendet',result['setup_steps'][1]['message'])
+ self.assertEqual(result['setup_steps'][-1]['state'],'blocked')
+ unknown=self.check('voice','unknown/model','model.safetensors')
+ self.assertTrue(all(s['state']=='blocked' for s in unknown['setup_steps']))
+ ready=assess(dict(kind='image',repo=QWEN_REPO,file='model.gguf'),{'image':True},dict(runnable=True))
+ self.assertTrue(all(s['state']=='complete' for s in ready['setup_steps']))