Guide model execution setup across catalog library and profiles
This commit is contained in:
@@ -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/<job-id>/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.
|
||||
|
||||
+9
-4
@@ -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 '<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>`;
|
||||
}
|
||||
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>
|
||||
<div id="hub-capacity" class="small muted"></div><div id="hub-job"></div><p id="hub-message" role="status" aria-live="polite"></p>
|
||||
@@ -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 => `<article class="card"><span class="pill">${escape(x.role_label)} · HERUNTERGELADEN</span><h3>${escape(x.repo)}</h3><p class="hub-filename">${escape(x.file)}</p><strong>${size(x.size)}</strong><p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${x.profile_eligible===true?`<div class="card-actions"><button data-profile-model="${x.id}">Profil anlegen</button></div>`:""}</article>`).join('') || '<div class="empty wide"><h3>Deine Bibliothek ist noch leer</h3><p>Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.</p></div>';
|
||||
el('hub-results').innerHTML = state.entries.filter(x => x.kind === kind).map(x => `<article class="card"><span class="pill">${escape(x.role_label)} · HERUNTERGELADEN</span><h3>${escape(x.repo)}</h3><p class="hub-filename">${escape(x.file)}</p><strong>${size(x.size)}</strong><p>${x.profile_eligible?'Modelldatei · über ein Profil verwenden':'Zusatzdatei · wird einem Modellprofil unter Komponenten zugeordnet'}</p><details><summary>Version und Integrität</summary><p class="hub-filename">Revision ${escape(x.revision)}</p><p>${x.upstream_hash_verified ? 'SHA-256 mit Quelle geprüft' : 'SHA-256 lokal erfasst; Größe mit Quelle geprüft'}</p></details>${executionHTML(x.execution)}${x.profile_eligible===true?`<div class="card-actions"><button data-profile-model="${x.id}">Profil anlegen</button></div>`:""}</article>`).join('') || '<div class="empty wide"><h3>Deine Bibliothek ist noch leer</h3><p>Unter „Entdecken“ kannst du Dateien für diesen Bereich auswählen und herunterladen.</p></div>';
|
||||
}
|
||||
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):'<p>Datei auswählen, um die passende Laufzeit zu prüfen.</p>';
|
||||
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 = '<p role="status">Modelldetails werden geladen …</p>';
|
||||
try {
|
||||
const result = await api('/files?'+new URLSearchParams({repo})); if (!root.isConnected || sequence !== detailSequence) return; detail = result;
|
||||
el('hub-files').innerHTML = `<span class="kicker">MODELLDETAILS</span><h2 class="hub-filename">${escape(repo)}</h2><div class="hub-badges"><span class="pill">${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}</span><span class="pill">${detail.files.length} unterstützte Dateien</span></div><p>Vision: ${escape(detail.capabilities?.vision||'Unbekannt')} · Tool-Calling: ${escape(detail.capabilities?.tools||'Unbekannt')}. Hub-Hinweise, keine Laufzeitgarantie; fehlende Angaben bedeuten nicht „nein“.</p><p>Lizenz: ${escape(detail.license || 'Nicht angegeben')} · <a class="link" href="${escape(detail.url)}" target="_blank" rel="noopener noreferrer">Modellkarte lesen ↗</a></p><div class="hub-fit" id="hub-fit-result">Wähle eine Datei für die Speicherprüfung.</div>${detail.gated ? '<p class="warning">Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.</p>' : ''}<h3>Variante oder Datei auswählen</h3><div class="hub-file-controls"><label>Dateiname filtern<input id="hub-file-filter" placeholder="z. B. Q4_K_M"></label><label>Format<select id="hub-format"><option value="">Alle Formate</option><option value=".gguf">GGUF</option><option value=".safetensors">Safetensors</option><option value=".json">JSON / Konfiguration</option></select></label></div><div id="hub-variants" class="hub-variants"></div><div class="hub-review"><p id="hub-selection" class="hub-filename"></p><button id="hub-download" disabled>Ausgewählte Datei herunterladen</button><p class="small">Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.</p><details><summary>Feste Quellversion</summary><p class="hub-filename">${escape(detail.revision)}</p></details></div>`;
|
||||
const result = await api('/files?'+new URLSearchParams({repo,kind})); if (!root.isConnected || sequence !== detailSequence) return; detail = result;
|
||||
el('hub-files').innerHTML = `<span class="kicker">MODELLDETAILS</span><h2 class="hub-filename">${escape(repo)}</h2><div class="hub-badges"><span class="pill">${detail.gated ? 'Zugang beschränkt' : 'Öffentlich'}</span><span class="pill">${detail.files.length} unterstützte Dateien</span></div><p>Vision: ${escape(detail.capabilities?.vision||'Unbekannt')} · Tool-Calling: ${escape(detail.capabilities?.tools||'Unbekannt')}. Hub-Hinweise, keine Laufzeitgarantie; fehlende Angaben bedeuten nicht „nein“.</p><p>Lizenz: ${escape(detail.license || 'Nicht angegeben')} · <a class="link" href="${escape(detail.url)}" target="_blank" rel="noopener noreferrer">Modellkarte lesen ↗</a></p><div id="hub-execution"></div><div class="hub-fit" id="hub-fit-result">Wähle eine Datei für die Speicherprüfung.</div>${detail.gated ? '<p class="warning">Dieses Repository benötigt eine Freigabe. Downloads mit Hugging-Face-Zugangsdaten werden noch nicht unterstützt.</p>' : ''}<h3>Variante oder Datei auswählen</h3><div class="hub-file-controls"><label>Dateiname filtern<input id="hub-file-filter" placeholder="z. B. Q4_K_M"></label><label>Format<select id="hub-format"><option value="">Alle Formate</option><option value=".gguf">GGUF</option><option value=".safetensors">Safetensors</option><option value=".json">JSON / Konfiguration</option></select></label></div><div id="hub-variants" class="hub-variants"></div><div class="hub-review"><p id="hub-selection" class="hub-filename"></p><button id="hub-download" disabled>Ausgewählte Datei herunterladen</button><p class="small">Einzeldatei-Download auf Athena. Geteilte Modelle und Bild-/Audio-/Video-Pipelines benötigen weitere Dateien; dies installiert noch keinen vollständigen Worker.</p><details><summary>Feste Quellversion</summary><p class="hub-filename">${escape(detail.revision)}</p></details></div>`;
|
||||
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};
|
||||
})();
|
||||
|
||||
+1
-1
@@ -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 \
|
||||
|
||||
+1
-1
@@ -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):
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${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><details><summary>${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${kind==='image'?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
el('profile-list').innerHTML=rows.map(p=>`<article class="card"><span class="pill">GESPEICHERT AUF ATHENA</span><h3>${e(p.name)}</h3><p class="hub-filename">${e(p.model?.file||'Modelldatei nicht verfügbar')}</p><p>${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>${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?'Laufzeit bereit · lädt bei Anfrage':'Was fehlt zur Ausführung?'}</summary>${p.blockers.map(t=>`<p>${e(t)}</p>`).join('')}</details><div class="card-actions"><label class="endpoint-choice"><input type="checkbox" data-publish="${p.id}" ${enabled.includes(p.id)?'checked':''} ${!p.runnable&&!enabled.includes(p.id)?'disabled':''}> Am API-Endpunkt freigegeben</label><button class="secondary" data-edit="${p.id}">Bearbeiten</button><button class="secondary" data-copy="${p.id}">Duplizieren</button><button class="secondary" data-delete="${p.id}">Löschen</button>${kind==='image'?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}</div></article>`).join('')||`<section class="empty"><h3>Noch keine Profile</h3><p>${models.length?'Wähle eine heruntergeladene Modelldatei und lege dein erstes Profil an.':'Lade zuerst eine Gewichtsdatei unter Entdecken herunter.'}</p></section>`;
|
||||
el('profile-list').insertAdjacentHTML('afterbegin',`<p>${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.</p>`);
|
||||
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;}});
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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 `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>Laufzeiten führen Modelle aus. Textencoder und VAE werden separat in der Modellbibliothek verwaltet und einem Profil zugeordnet.</p><div class="grid"><section class="card"><h2>Text-to-Speech</h2><p>Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.</p><a class="link" href="#tts-runtime">TTS einrichten →</a></section><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
||||
function runtimeOverview(){return `<div class="kicker">EINSTELLUNGEN</div><h1>Laufzeiten</h1><p>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.</p><div class="grid"><section class="card"><h2>Text-to-Speech</h2><p>Qwen3-TTS mit CUDA und eingebauten Stimmen einrichten.</p><a class="link" href="#tts-runtime">TTS einrichten →</a></section><section class="card"><h2>llama.cpp</h2><p>Sprachmodelle · CPU / CUDA. Versionen herunterladen, bauen und verwalten.</p><a class="link" href="#runtime">llama.cpp öffnen →</a></section><section class="card"><h2>Bildgenerierung</h2><p>Eigene Bildlaufzeit und modellabhängige Zusatzkomponenten.</p><a class="link" href="#image-runtime">Bildlaufzeit öffnen →</a></section><section class="card"><h2>Docker</h2><p>Optionale Container-Umgebung für zusätzliche Oberflächen und Studios.</p><a class="link" href="#docker-runtime">Docker öffnen →</a></section></div>`;}
|
||||
function imageRuntime(){return `<div class="kicker">EINSTELLUNGEN / LAUFZEITEN</div><h1>Bildgenerierung</h1><section class="card"><h2>ComfyUI + ComfyUI-GGUF</h2><p>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.</p><p>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.</p><div id="image-runtime-install"><p>Installationsstatus wird geprüft …</p></div><a class="link" href="#image">Bildgenerierung → Testen öffnen</a></section>${ImageTestUI.html(true)}`;}
|
||||
function openProfile(page,modelId){render(page,true,modelId);}
|
||||
return {render,openProfile};
|
||||
|
||||
@@ -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')
|
||||
+1
-1
@@ -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.')
|
||||
|
||||
Reference in New Issue
Block a user