From 7bd9096e672f2c73402a860cbfde9857ea2ec6ad Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Tue, 29 Sep 2026 08:27:19 +0200 Subject: [PATCH] Guide model execution setup across catalog library and profiles --- STUDIO.md | 8 ++++++++ catalog-ui.js | 13 +++++++++---- deploy/Dockerfile | 2 +- deploy/install.py | 2 +- execution_setup.py | 32 ++++++++++++++++++++++++++++++++ network/install_remote.py | 2 +- profiles-ui.js | 2 +- server.py | 16 +++++++++++++--- studio.js | 2 +- test_execution_setup.py | 27 +++++++++++++++++++++++++++ tts_test.py | 2 +- 11 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 execution_setup.py create mode 100644 test_execution_setup.py diff --git a/STUDIO.md b/STUDIO.md index 1edba16..3006696 100644 --- a/STUDIO.md +++ b/STUDIO.md @@ -235,3 +235,11 @@ Unter **TTS → Testen** Profil, Stimme und Sprache wählen, Text eingeben und e Text wird nur über stdin übertragen und nicht gespeichert. WAV-Dateien bleiben unter `tts-tests//result.wav` im privaten Deck-Zustand; es gibt noch keine automatische Aufräumfunktion. Audio ist nur nach Anmeldung abrufbar. Die API-Freigabe erfolgt wie bei anderen Profilen ausdrücklich über den Endpunkt-Schalter. Interne authentifizierte API: `GET /api/v1/tts`, `GET /api/v1/tts/audio?id=…`, `POST /api/v1/tts/install`, `/install-cancel`, `/assign` (`id`, `revision`), `/start` (`profile_id`, `text`, optional `speaker`, `language`) und `/cancel`. + +### Gemeinsame Laufzeitprüfung + +Dateiauswahl unter Entdecken, Bibliothek und Profile zeigen jetzt einen gemeinsamen Einrichtungsstatus. Die lesende Zuordnung in `execution_setup.py` verwendet die vorhandenen Bildrezepte, die festgelegte CustomVoice-Revision und Chat-GGUF als llama.cpp-Kandidaten. Ein Chat-GGUF ist ohne Ladetest keine Zusage über Architekturunterstützung. STT, Musik, Voice, Video und unbekannte Bild-/TTS-Varianten bleiben ausdrücklich nicht unterstützt. + +„Ausführung einrichten“ führt zum passenden vorhandenen Installer. Ist die Umgebung installiert, wird sie wiederverwendet und „Einrichtung ansehen“ angeboten. Bei Bildprofilen führt „Komponenten“ zu Textencoder-/VAE-Download und Zuordnung. TTS installiert sein vollständiges Modellpaket; ein alternatives TTS-Modell wird nie stillschweigend ersetzt. Konfigurationen, Projektoren, Encoder und VAE sind Zusatzdateien ohne eigenen Worker. Es werden weder beliebiger Installationscode gesucht noch Modelle automatisch gestartet. + +Die Verwaltungsantworten für Katalogdateien, Bibliothek und Profile enthalten `execution` mit Zustand, Erklärung, Laufzeit, Installationsstatus und festem Einrichtungslink. `/api/v1/catalog/files` akzeptiert dafür `kind`. Speicherpassung bleibt eine separate Prüfung. Der Download nicht unterstützter Modelle bleibt für spätere Verwendung möglich. diff --git a/catalog-ui.js b/catalog-ui.js index 53a09d0..e6bd438 100644 --- a/catalog-ui.js +++ b/catalog-ui.js @@ -7,6 +7,10 @@ window.CatalogUI = (() => { const response = await fetch('/api/v1/catalog'+path, data ? {method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)} : {}); const result = await response.json(); if (!response.ok) throw Error(result.error || 'Anfrage fehlgeschlagen'); return result; } + 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.

`; + } 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.'}

@@ -26,7 +30,7 @@ window.CatalogUI = (() => { root.querySelectorAll('[data-repo]').forEach(b => b.onclick = () => loadFiles(b.dataset.repo)); } function renderLibrary() { - el('hub-results').innerHTML = state.entries.filter(x => x.kind === kind).map(x => `
${escape(x.role_label)} · HERUNTERGELADEN

${escape(x.repo)}

${escape(x.file)}

${size(x.size)}

${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}

Version und Integrität

Revision ${escape(x.revision)}

${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}

${x.profile_eligible===true?`
`:""}
`).join('') || '

Deine Bibliothek ist noch leer

Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.

'; + el('hub-results').innerHTML = state.entries.filter(x => x.kind === kind).map(x => `
${escape(x.role_label)} · HERUNTERGELADEN

${escape(x.repo)}

${escape(x.file)}

${size(x.size)}

${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}

Version und Integrität

Revision ${escape(x.revision)}

${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}

${executionHTML(x.execution)}${x.profile_eligible===true?`
`:""}
`).join('') || '

Deine Bibliothek ist noch leer

Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.

'; } function renderJob() { el('hub-capacity').textContent = `Athena · ${size(state.free_bytes)} freier Speicher · 10 GiB bleiben als Reserve frei`; @@ -57,6 +61,7 @@ window.CatalogUI = (() => { if (!detail || !el('hub-download')) return; const chosen = el('hub-variants').querySelector('input:checked'); const file = chosen ? detail.files[Number(chosen.value)] : null; + if(el('hub-execution'))el('hub-execution').innerHTML=file?executionHTML(file.execution):'

Datei auswählen, um die passende Laufzeit zu prüfen.

'; const busy = starting; const queued=file && (state.downloads||[]).some(j=>['queued','downloading'].includes(j.state)&&j.repo===detail.repo&&j.revision===detail.revision&&j.file===file.name); const exists = file && installed(file); @@ -77,8 +82,8 @@ window.CatalogUI = (() => { const sequence = ++detailSequence; selected = repo; detail = null; renderModels(); el('hub-files').innerHTML = '

Modelldetails werden geladen …

'; try { - const result = await api('/files?'+new URLSearchParams({repo})); if (!root.isConnected || sequence !== detailSequence) return; detail = result; - el('hub-files').innerHTML = `MODELLDETAILS

${escape(repo)}

${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}${detail.files.length} unterstützte Dateien

Vision: ${escape(detail.capabilities?.vision||'Unbekannt')} · Tool-Calling: ${escape(detail.capabilities?.tools||'Unbekannt')}. Hub-Hinweise, keine Laufzeitgarantie; fehlende Angaben bedeuten nicht „nein“.

Lizenz: ${escape(detail.license || 'Nicht angegeben')} · Modellkarte lesen ↗

Wähle eine Datei für die Speicherprüfung.
${detail.gated ? '

Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.

' : ''}

Variante oder Datei auswählen

Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.

Feste Quellversion

${escape(detail.revision)}

`; + const result = await api('/files?'+new URLSearchParams({repo,kind})); if (!root.isConnected || sequence !== detailSequence) return; detail = result; + el('hub-files').innerHTML = `MODELLDETAILS

${escape(repo)}

${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}${detail.files.length} unterstützte Dateien

Vision: ${escape(detail.capabilities?.vision||'Unbekannt')} · Tool-Calling: ${escape(detail.capabilities?.tools||'Unbekannt')}. Hub-Hinweise, keine Laufzeitgarantie; fehlende Angaben bedeuten nicht „nein“.

Lizenz: ${escape(detail.license || 'Nicht angegeben')} · Modellkarte lesen ↗

Wähle eine Datei für die Speicherprüfung.
${detail.gated ? '

Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.

' : ''}

Variante oder Datei auswählen

Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.

Feste Quellversion

${escape(detail.revision)}

`; el('hub-file-filter').oninput = renderFiles; el('hub-format').onchange = renderFiles; renderFiles(); el('hub-download').onclick = async () => { const chosen = el('hub-variants').querySelector('input:checked'); if (!chosen || starting) return; @@ -94,5 +99,5 @@ window.CatalogUI = (() => { if (!library && !downloads) { try { el('hub-base-only').checked=localStorage.getItem('deck-base-only')==='true'; } catch {} el('hub-base-only').onchange=()=>{try { localStorage.setItem('deck-base-only',String(el('hub-base-only').checked)); } catch {} search(new FormData(el('hub-search')).get('q').trim());}; el('hub-sort').onchange = () => search(new FormData(el('hub-search')).get('q').trim()); el('hub-search').onsubmit = event => { event.preventDefault(); search(new FormData(event.target).get('q').trim()); }; search(''); } refresh(); } - return {html,bind}; + return {html,bind,executionHTML}; })(); diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 26efe79..135dbb9 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -13,7 +13,7 @@ RUN git clone https://github.com/Comfy-Org/ComfyUI.git /opt/deck-comfy \ WORKDIR /app COPY deploy/image-requirements.lock /app/deploy/image-requirements.lock COPY deploy/tts-requirements.lock /app/deploy/tts-requirements.lock -COPY tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py profiles.py capacity.py server.py runtime.py catalog.py auth.py collect_hardware.py /app/ +COPY execution_setup.py tts_runtime.py tts_test.py tts_worker.py auto_test.py chat_test.py endpoint.py inference.py docker_support.py image_encoder_node.py image_runtime.py image_test.py profiles.py capacity.py server.py runtime.py catalog.py auth.py collect_hardware.py /app/ COPY tts-ui.js auto-test-ui.js chat-test-ui.js endpoint-ui.js docker-ui.js image-test-ui.js profiles-ui.js index.html app.js studio.js runtime-ui.js catalog-ui.js style.css login.html login.js access-ui.js network-ui.js /app/ COPY network/__init__.py network/client.py network/config.py network/rpc.py /app/network/ ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 HOME=/tmp \ diff --git a/deploy/install.py b/deploy/install.py index a9dc26a..a6c2614 100644 --- a/deploy/install.py +++ b/deploy/install.py @@ -14,7 +14,7 @@ import urllib.request ROOT = Path(__file__).resolve().parent.parent LABEL = 'de.casaderoll.athena-deck.standalone' -FILES = ['deploy/tts-requirements.lock','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','catalog.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] +FILES = ['deploy/tts-requirements.lock','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','docker_support.py','docker-ui.js','deploy/docker_helper.py','deploy/setup_docker_helper.py','image_encoder_node.py','image_runtime.py','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py','runtime-ui.js','catalog.py','catalog-ui.js','server.py','auth.py','collect_hardware.py','index.html','app.js','studio.js','style.css','login.html','login.js','access-ui.js','network-ui.js','network/__init__.py','network/client.py','network/config.py','network/rpc.py','deploy/Dockerfile','deploy/image-requirements.lock'] def run(*args, check=True, interactive=False): diff --git a/execution_setup.py b/execution_setup.py new file mode 100644 index 0000000..d11e047 --- /dev/null +++ b/execution_setup.py @@ -0,0 +1,32 @@ +"""Read-only, conservative mapping to Deck's maintained execution adapters.""" +from catalog import file_role +from profiles import image_recipe +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.') + 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=='image' and image_recipe(model): + 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.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=='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 not result['runtime']:return result + result['installed']=bool(installed.get(kind)) + if kind!='chat': + 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']: + result.update(state='runtime_missing',label='Passende Laufzeit fehlt') + if profile: + result['blockers']=profile.get('blockers',[]) + if profile.get('runnable'): + result.update(state='ready',label='Ausführbar – Laufzeit eingerichtet') + elif result['installed']: + result.update(state='profile_incomplete',label='Profil prüfen – Voraussetzungen fehlen') + return result diff --git a/network/install_remote.py b/network/install_remote.py index 4ae758d..db9c50b 100644 --- a/network/install_remote.py +++ b/network/install_remote.py @@ -12,7 +12,7 @@ import sys NAME = 'athena-deck-network' BASE = Path('/opt/athena-deck') -FILES = {'deploy/tts-requirements.lock','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','image_runtime.py','image_encoder_node.py','docker_support.py','docker-ui.js','deploy/image-requirements.lock','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py', 'runtime-ui.js', 'catalog.py', 'catalog-ui.js', 'studio.js', 'auth.py', 'access-ui.js', 'server.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'} +FILES = {'deploy/tts-requirements.lock','execution_setup.py','tts_runtime.py','tts_test.py','tts_worker.py','tts-ui.js','auto_test.py','auto-test-ui.js','chat_test.py','chat-test-ui.js','endpoint.py','inference.py','endpoint-ui.js','image_runtime.py','image_encoder_node.py','docker_support.py','docker-ui.js','deploy/image-requirements.lock','image_test.py','image-test-ui.js','profiles.py','profiles-ui.js','capacity.py','runtime.py', 'runtime-ui.js', 'catalog.py', 'catalog-ui.js', 'studio.js', 'auth.py', 'access-ui.js', 'server.py', 'collect_hardware.py', 'index.html', 'app.js', 'style.css', 'network-ui.js', 'login.html', 'login.js', 'network/__init__.py', 'network/config.py', 'network/policy.py', 'network/rpc.py', 'network/agent.py', 'network/client.py', 'network/Dockerfile', '.dockerignore'} def run(*args, **kwargs): return subprocess.run(args, capture_output=True, timeout=600, **kwargs) diff --git a/profiles-ui.js b/profiles-ui.js index f25b8e9..276353f 100644 --- a/profiles-ui.js +++ b/profiles-ui.js @@ -7,7 +7,7 @@ window.ProfilesUI=(()=>{ const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},enabled=[],gpus=[],projectors=[],panelSequence=0; const message=t=>{if(root.isConnected)el('profile-message').textContent=t;}; async function load(){try{const [p,c,h,ep]=await Promise.all([api('profiles'),api('catalog'),kind==='chat'?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);projectors=c.entries.filter(x=>x.role==='vision_projector'&&x.file.endsWith('.gguf'));enabled=ep.profiles.filter(x=>x.enabled).map(x=>x.id);gpus=h.gpus||[];if(!root.isConnected)return;rows=p.profiles.filter(x=>x.kind===kind);models=c.entries.filter(x=>x.kind===kind&&x.profile_eligible===true);schema=p.schemas[kind];el('profile-new').disabled=!models.length; - el('profile-list').innerHTML=rows.map(p=>`
GESPEICHERT AUF ATHENA

${e(p.name)}

${e(p.model?.file||'Modelldatei nicht verfügbar')}

${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}

${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}${p.blockers.map(t=>`

${e(t)}

`).join('')}
${kind==='image'?``:''}
`).join('')||`

Noch keine Profile

${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}

`; + el('profile-list').innerHTML=rows.map(p=>`
GESPEICHERT AUF ATHENA

${e(p.name)}

${e(p.model?.file||'Modelldatei nicht verfügbar')}

${Object.entries(p.parameters).map(([k,v])=>`${e(labels[k]||k)}: ${e(k==='gpu_reserve_mode'?({auto:'Automatisch',manual:'Manuell',none:'Keine zusätzliche Reserve'}[v]||v):k==='gpu_reserve_mib'?(Object.entries(v).map(([id,n])=>`${gpus.find(g=>g.uuid===id)?.name||id}: ${n} MiB`).join(', ')||'gemäß Reservemodus'):k==='gpu_devices'?(v.length?v.map((id,i)=>`CUDA${i}: ${gpus.find(g=>g.uuid===id)?.name||id}`).join(', '):'Automatisch'):Array.isArray(v)?(v.join(', ')||'Automatisch'):v)}`).join(' · ')}

${CatalogUI.executionHTML(p.execution)}
${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}${p.blockers.map(t=>`

${e(t)}

`).join('')}
${kind==='image'?``:''}
`).join('')||`

Noch keine Profile

${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}

`; el('profile-list').insertAdjacentHTML('afterbegin',`

${kind==='image'?'Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':kind==='audio'?'Eingerichtete TTS-Profile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':'Für diesen Bereich ist noch keine ausführbare Laufzeit am Endpunkt angebunden.'} Freigegeben bedeutet nicht bereits in den GPU-Speicher geladen.

`); root.querySelectorAll('[data-publish]').forEach(b=>b.onchange=async()=>{const checked=b.checked;root.querySelectorAll('[data-publish]').forEach(x=>x.disabled=true);try{await api('endpoint/profile',{id:b.dataset.publish,enabled:checked});await load();message('Endpunkt-Freigabe gespeichert. Geladen wird erst bei einer API-Anfrage.');}catch(error){await load();message(error.message);}}); root.querySelectorAll('[data-delete]').forEach(b=>b.onclick=async()=>{const p=rows.find(p=>p.id===b.dataset.delete);if(!confirm(`Profil „${p.name}“ löschen? Modelldateien und Komponenten bleiben in der Bibliothek.`))return;b.disabled=true;try{await api('profiles/delete',{id:p.id,revision:p.revision});panelSequence++;el('profile-editor').replaceChildren();await load();message('Profil gelöscht. Modelldateien und Komponenten bleiben erhalten.');}catch(error){message(error.message);b.disabled=false;}}); diff --git a/server.py b/server.py index 95e665b..e6dc794 100644 --- a/server.py +++ b/server.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 """Athena Deck prototype: loopback API, owned model processes, read-only telemetry.""" +from execution_setup import assess as execution_assess import argparse import os import hashlib @@ -239,6 +240,10 @@ class Handler(BaseHTTPRequestHandler): allowed.update(filter(None,os.environ.get('DECK_ALLOWED_HOSTS','').split(','))) return self.headers.get('Host') in allowed + def execution_setup(self, model, profile=None): + installed={'chat':bool(self.server.runtime.status().get('active')),'image':self.server.image_runtime.status().get('installed',False),'audio':self.server.tts_runtime.status().get('installed',False)} + return execution_assess(model,installed,profile) + def do_GET(self): if not self.valid_host(): return self.respond({'error': 'Host rejected'}, 403) @@ -276,7 +281,9 @@ class Handler(BaseHTTPRequestHandler): except ValueError as exc:return self.respond({'error':str(exc)},400) except Exception:return self.respond({'error':'Komponentenquelle nicht erreichbar. Bitte erneut versuchen.'},503) if self.path == '/api/v1/profiles': - return self.respond(self.server.profiles.status()) + data=self.server.profiles.status() + for p in data['profiles']:p['execution']=self.execution_setup(p.get('model') or {},p) + return self.respond(data) if self.path == '/api/v1/hardware': return self.respond(self.server.hardware.snapshot()) if self.path.startswith('/api/v1/runtime'): @@ -296,9 +303,12 @@ 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'])) 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]))) for f in data['files']] + return self.respond(data) + if path.path=='/api/v1/catalog': + data=self.server.catalog.status() + for entry in data['entries']:entry['execution']=self.execution_setup(entry) return self.respond(data) - if path.path=='/api/v1/catalog':return self.respond(self.server.catalog.status()) except Exception as exc: return self.respond({'error':str(exc) if isinstance(exc,ValueError) else 'Katalog nicht erreichbar.'},400) if self.path == '/api/v1/network': diff --git a/studio.js b/studio.js index 503dff3..5a92a7d 100644 --- a/studio.js +++ b/studio.js @@ -24,7 +24,7 @@ const Studio=(()=>{ else if(section!=='running')CatalogUI.bind(page,section==='library',section==='downloads'); document.querySelectorAll('[data-section]').forEach(b=>b.onclick=()=>{section=b.dataset.section;render(page,true);}); } - function runtimeOverview(){return `
EINSTELLUNGEN

Laufzeiten

Laufzeiten führen Modelle aus. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.

Text-to-Speech

Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.

TTS einrichten →

llama.cpp

Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.

llama.cpp öffnen →

Bildgenerierung

Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.

Bildlaufzeit öffnen →

Docker

Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.

Docker öffnen →
`;} + function runtimeOverview(){return `
EINSTELLUNGEN

Laufzeiten

Laufzeiten werden einmal eingerichtet und für kompatible Modelle wiederverwendet. Entdecken, Bibliothek und Profile zeigen den passenden Einrichtungsweg; unbekannte Modelle werden als noch nicht unterstützt gekennzeichnet. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.

Text-to-Speech

Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.

TTS einrichten →

llama.cpp

Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.

llama.cpp öffnen →

Bildgenerierung

Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.

Bildlaufzeit öffnen →

Docker

Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.

Docker öffnen →
`;} function imageRuntime(){return `
EINSTELLUNGEN / LAUFZEITEN

Bildgenerierung

ComfyUI + ComfyUI-GGUF

Eigene Bildlaufzeit für Qwen-Image-2.1 GGUF und das unterstützte FLUX.2-Klein-9B-Modell. Die Umgebung kann über den Deck-Installer mitgeliefert oder hier separat installiert werden. Modelle laden erst bei einem Testauftrag.

Textencoder läuft auf der RTX 3060, Diffusionsmodell und VAE auf der RTX 5080. Bei großen FLUX-Gewichten wird zusätzlich System-RAM genutzt. Beide GPUs müssen frei sein. Pro Test wird ein eigener Prozess gestartet und anschließend beendet.

Installationsstatus wird geprüft …

Bildgenerierung → Testen öffnen
${ImageTestUI.html(true)}`;} function openProfile(page,modelId){render(page,true,modelId);} return {render,openProfile}; diff --git a/test_execution_setup.py b/test_execution_setup.py new file mode 100644 index 0000000..24f8319 --- /dev/null +++ b/test_execution_setup.py @@ -0,0 +1,27 @@ +import unittest +from execution_setup import assess +from profiles import QWEN_REPO,FLUX_REPO +from tts_runtime import REPO,REVISION +class ExecutionSetupTests(unittest.TestCase): + def check(self,kind,repo,file,installed=None,**extra):return assess(dict(kind=kind,repo=repo,file=file,**extra),installed or {}) + def test_unknown_and_asr_not_llama(self): + for kind in ['stt','music','voice','video','image','audio']: + self.assertEqual(self.check(kind,'other/model','model.gguf')['state'],'unsupported') + def test_images_reuse_runtime_but_need_components(self): + for repo,file in [(QWEN_REPO,'model.gguf'),(FLUX_REPO,'Flux.2 Klein-9B_fp16_nsfw.safetensors')]: + r=self.check('image',repo,file,{'image':True});self.assertEqual(r['state'],'setup');self.assertEqual(len(r['components']),2) + self.assertEqual(self.check('image',repo,file)['state'],'runtime_missing') + def test_tts_exact_revision_no_silent_replacement(self): + self.assertEqual(self.check('audio',REPO,'model.safetensors',revision=REVISION)['state'],'runtime_missing') + self.assertEqual(self.check('audio',REPO,'model.safetensors',revision='other')['state'],'unsupported') + self.assertIn('alternative',self.check('audio','mlx-community/base','model.safetensors')) + def test_components(self): + for file in ['mmproj.gguf','vae/model.safetensors','speech_tokenizer/model.safetensors','config.json']: + self.assertEqual(self.check('audio',REPO,file)['state'],'component') + def test_chat_is_candidate_not_guarantee(self): + self.assertEqual(self.check('chat','a/b','model.gguf',{'chat':True})['state'],'check') + self.assertEqual(self.check('chat','a/b','model.safetensors')['state'],'unsupported') + def test_profile_blockers_and_ready(self): + 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') diff --git a/tts_test.py b/tts_test.py index 3a7f2c3..1ecada4 100644 --- a/tts_test.py +++ b/tts_test.py @@ -9,7 +9,7 @@ class TTSTests: def status(self): with self.lock:return dict(job=dict(self.job) if self.job else None,runtime=self.runtime.status()) def blockers(self,p): - if p['model']['repo']!=REPO or p['model']['file']!='model.safetensors' or p['model'].get('revision')!=REVISION:return ['Dieses Modell ist nicht CUDA-CustomVoice. Unter TTS → Einrichten die passende Variante installieren und dem Profil zuordnen.'] + if p['model']['repo']!=REPO or p['model']['file']!='model.safetensors' or p['model'].get('revision')!=REVISION:return ['Für diese TTS-Variante fehlt die Anbindung in Deck. Unter TTS → Einrichten kannst du ausdrücklich auf die unterstützte Variante mit eingebauten Stimmen wechseln. Die ursprüngliche Datei bleibt erhalten.'] return [] if self.runtime.status()['installed'] else ['TTS-Laufzeit unter TTS → Einrichten installieren.'] def assign(self,ident,revision): if not self.runtime.status()['installed']:raise ValueError('Zuerst TTS einrichten.')