Unify component setup across categories and retain two installation backups
This commit is contained in:
@@ -51,7 +51,7 @@ def validate(doc):
|
||||
if kind=='chat' and (params['ubatch']>params['batch'] or params.get('vision_projector') and params['vision_projector'] not in ids):raise ValueError('Ungültige Chat-Zuordnung.')
|
||||
if kind in ('image','video') and (params['width']%64 or params['height']%64):raise ValueError('Ungültige Bildgröße.')
|
||||
components=p.get('components',{})
|
||||
if not isinstance(components,dict) or any(k not in ('text_encoder','vae','video_vae','audio_vae','spatial_upsampler') or v not in ids for k,v in components.items()):raise ValueError('Ungültige Komponentenreferenz.')
|
||||
if not isinstance(components,dict) or any((k not in ('text_encoder','vae','video_vae','audio_vae','spatial_upsampler','model_config','generation_config','tokenizer','vae_config','manual_text_encoder','manual_vae','manual_tokenizer','manual_configuration','manual_projector','manual_auxiliary') and not re.fullmatch(r'runtime_file_[0-9]{1,2}',k)) or v not in ids for k,v in components.items()):raise ValueError('Ungültige Komponentenreferenz.')
|
||||
pids.add(p['id']);names.add(name)
|
||||
settings=doc.get('settings')
|
||||
if not isinstance(settings,dict) or set(settings)-set(SETTINGS):raise ValueError('Unbekannte Einstellungsdateien.')
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ window.CatalogUI = (() => {
|
||||
}
|
||||
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><p>Modellfamilie: ${escape(x.family||'Unbekannt')}${x.evidence?' · '+escape(x.evidence):''}</p>${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(' · ')}. Modellabhängig zuordnen; benötigte Zusatzdateien gehören zum vollständigen Paket.</p>`:''}${x.route?`<a class="link" href="${escape(x.route)}">${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →</a>`:''}${x.candidates?.length?`<h4>Mögliche Laufzeiten</h4><p class="small">Diese Liste unterscheidet bekannte Zuordnungen von ungeprüften Kategorieoptionen. Eine Installation ersetzt keinen fehlenden Worker.</p>${x.candidates.map(c=>`<p><strong>${escape(c.label)}</strong> · ${c.installed?'bereits installiert – wiederverwenden':'nicht installiert'}<br>${escape(c.evidence)}${c.route?`<br><a class="link" href="${escape(c.route)}">${c.installed?'Laufzeit / Einrichtung ansehen':'Laufzeit herunterladen / bereitmachen'} →</a>`:'<br>Kein gepflegter Deck-Installer für diese Option verfügbar.'}</p>`).join('')}`:`<p>${escape(x.candidate_message||'')}</p>`}${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>`;
|
||||
return `<section class="note execution-setup"><strong>${escape(x.label)}</strong><p>Modellfamilie: ${escape(x.family||'Unbekannt')}${x.evidence?' · '+escape(x.evidence):''}</p>${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(' · ')}. Modellabhängig zuordnen; benötigte Zusatzdateien gehören zum vollständigen Paket.</p>`:''}${x.suggested_components?.length?`<p>Zusatzdateien in dieser Quelle (Kompatibilität ungeprüft): ${x.suggested_components.map(escape).join(' · ')}. Nach dem Download unter Profil → Einrichtung & Komponenten prüfen.</p>`:''}${x.route?`<a class="link" href="${escape(x.route)}">${x.installed?'Einrichtung ansehen':'Ausführung einrichten'} →</a>`:''}${x.candidates?.length?`<h4>Mögliche Laufzeiten</h4><p class="small">Diese Liste unterscheidet bekannte Zuordnungen von ungeprüften Kategorieoptionen. Eine Installation ersetzt keinen fehlenden Worker.</p>${x.candidates.map(c=>`<p><strong>${escape(c.label)}</strong> · ${c.installed?'bereits installiert – wiederverwenden':'nicht installiert'}<br>${escape(c.evidence)}${c.route?`<br><a class="link" href="${escape(c.route)}">${c.installed?'Laufzeit / Einrichtung ansehen':'Laufzeit herunterladen / bereitmachen'} →</a>`:'<br>Kein gepflegter Deck-Installer für diese Option verfügbar.'}</p>`).join('')}`:`<p>${escape(x.candidate_message||'')}</p>`}${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>
|
||||
|
||||
@@ -218,6 +218,9 @@ def update(base, rollback=False, force=False, api_ports=None, reuse_runtime=Fals
|
||||
if was_running:run('docker','start',config['name'])
|
||||
raise RuntimeError('Update fehlgeschlagen. Vorheriger Deck-Container wiederhergestellt.') from None
|
||||
run('docker','rm',backup_name)
|
||||
# Keep only two automatic pre-update state snapshots after a successful update.
|
||||
snapshots=sorted((p for p in (base/'backups').iterdir() if p.is_dir() and not p.is_symlink() and p.name.isdigit()),key=lambda p:int(p.name))
|
||||
for snapshot in snapshots[:-2]:shutil.rmtree(snapshot)
|
||||
print('Nur Athena Deck wurde aktualisiert. Vorheriges Image bleibt für --rollback erhalten.')
|
||||
|
||||
|
||||
|
||||
@@ -19,3 +19,11 @@ Interne Admin-API: `GET /api/v1/audio-cpp-runtime`, `GET /api/v1/audio-cpp-runti
|
||||
## YuE2-Komponenten in der GUI
|
||||
|
||||
Unter **Musik → Profile → Komponenten** bietet Deck für das offizielle YuE2-GGUF-Hauptmodell fünf Rollen: VAE (F16 empfohlen oder F32), Modellkonfiguration, Generierungskonfiguration, Qwen-Tokenizer und VAE-Konfiguration. „Alle fehlenden Dateien herunterladen“ reiht je eine Datei pro Rolle in die Download-Warteschlange ein. Danach „Bibliothek aktualisieren“ und „Zuordnung auf Athena speichern“. Bereits vorhandene Dateien werden wiederverwendet. Quelle und Zuweisung müssen dieselbe Hub-Revision wie das Hauptmodell haben. Diese Zusatzdateien sind keine eigenständigen Modelle. Der fehlende Musikworker bleibt auch nach vollständiger Zuweisung als Blocker sichtbar.
|
||||
|
||||
## Gemeinsame Einrichtung in allen Kategorien
|
||||
|
||||
Alle Profile haben „Einrichtung & Komponenten“. Zuerst wird ein gepflegtes Modellrezept verwendet. Fehlt dieses, können installierte audio.cpp-Paketdefinitionen für exakt passende audio-cpp-Repositories Zusatzdateien liefern. Ohne vollständiges Rezept zeigt Deck Dateinamen-Hinweise der Modellquelle als **ungeprüft** an. Ein anderer Hugging-Face-Link kann als Komponentenquelle ausgewählt werden. Vorhandene Zusatzdateien lassen sich in allen Kategorien zuordnen. Manuelle Rollen sind optional und werden niemals automatisch als kompatibel angenommen; ihre Zuordnung blockiert die Ausführung bis zur passenden Worker-Anbindung. Beim Wechsel zur manuellen Quelle bleiben vorhandene Rezept-Zuordnungen erhalten.
|
||||
|
||||
Die Entdecken-Dateiansicht zeigt benötigte Komponenten aus bekannten Rezepten und Laufzeit-Paketen bereits vor dem Hauptdownload; weitere sichtbare Projektoren, Encoder und VAE werden als ungeprüfte Hinweise aufgeführt. Es gibt keine universelle, vollständige Erkennung aller Hub-Modelle.
|
||||
|
||||
Download-Annahme und Fehler erscheinen direkt neben der betroffenen Komponente. Die 10-GiB-Diskreserve bleibt erhalten. Der Installer behält nach erfolgreichen Updates nur die zwei neuesten automatischen Zustands-Sicherungen; manuell exportierte Backup-Dateien werden nicht angefasst.
|
||||
|
||||
+7
-6
@@ -7,7 +7,7 @@ window.ProfilesUI=(()=>{
|
||||
const root=document.querySelector('#live-profiles'),el=id=>root.querySelector('#'+id);let rows=[],models=[],schema={},enabled=[],gpus=[],projectors=[],entries=[],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'),['chat','video'].includes(kind)?api('hardware').catch(()=>({gpus:[]})):Promise.resolve({gpus:[]}),api('endpoint')]);entries=c.entries;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(['video_device','text_encoder_device'].includes(k)?({auto:'Automatisch · RTX 5080 bevorzugt',same:'Wie Videopipeline'}[v]||gpus.find(g=>g.uuid===v)?.name||v):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>${kind==='image'?`<p>Prompt-Aufwerter: Text-zu-Bild ${p.prompt_enhancer?.t2i?'an':'aus'} · Bildbearbeitung ${p.prompt_enhancer?.i2i?'an':'aus'} · Gerät ${e(p.prompt_enhancer?.device||'auto')}</p>`:''}${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'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>${['image','video','music'].includes(kind)?`<button class="secondary" data-components="${p.id}">Komponenten</button>`:''}${kind==='image'?`<button class="secondary" data-enhancers="${p.id}">Prompt-Aufwerter</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(['video_device','text_encoder_device'].includes(k)?({auto:'Automatisch · RTX 5080 bevorzugt',same:'Wie Videopipeline'}[v]||gpus.find(g=>g.uuid===v)?.name||v):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>${kind==='image'?`<p>Prompt-Aufwerter: Text-zu-Bild ${p.prompt_enhancer?.t2i?'an':'aus'} · Bildbearbeitung ${p.prompt_enhancer?.i2i?'an':'aus'} · Gerät ${e(p.prompt_enhancer?.device||'auto')}</p>`:''}${CatalogUI.executionHTML(p.execution)}<details><summary>${p.runnable?(kind==='video'?'Laufzeit bereit · Video-Modus in Übersicht aktivieren':'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><button class="secondary" data-components="${p.id}">Einrichtung & Komponenten</button>${kind==='image'?`<button class="secondary" data-enhancers="${p.id}">Prompt-Aufwerter</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'?'Der feste API-Name athena-image verwendet das hier freigegebene Bildprofil und dessen Auflösung. Höchstens ein Bildprofil ist am API-Endpunkt freigegeben. Ein neuer Haken ersetzt die bisherige Auswahl.':['audio','stt'].includes(kind)?'Eingerichtete Sprachprofile können am API-Endpunkt freigegeben werden.':kind==='chat'?'Mehrere Sprachmodellprofile können gleichzeitig am API-Endpunkt freigegeben sein.':kind==='video'?'Genau ein Videoprofil kann am API-Endpunkt freigegeben sein. Ein neuer Haken ersetzt die bisherige Auswahl. Die Übersicht lädt dieses Profil beim Wechsel auf Video; API-Name: athena-video. Zum Ändern der Freigabe zuerst auf LLM wechseln.':'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(kind==='video'?'Video-Freigabe gespeichert. Die Übersicht lädt dieses Profil beim Wechsel auf Video.':'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;}});
|
||||
@@ -31,22 +31,23 @@ window.ProfilesUI=(()=>{
|
||||
}
|
||||
refresh();el('profile-editor').scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}
|
||||
async function components(profile){
|
||||
async function components(profile,extraRepo){
|
||||
const sequence=++panelSequence;
|
||||
el('profile-editor').innerHTML='<section class="card editor"><p role="status">Komponenten werden geprüft …</p></section>';
|
||||
try{
|
||||
const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id}));if(!root.isConnected||sequence!==panelSequence)return;
|
||||
el('profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p>${profile.kind==='image'?'<a class="link" href="#image-runtime">Bildlaufzeit →</a> · ':profile.kind==='music'?'<a class="link" href="#audio-cpp-runtime">Musiklaufzeit →</a> · ':'Videolaufzeit · '}${e(data.runtime)} · ${data.runtime_installed?'installiert':'noch nicht eingerichtet'}</p><button type="button" id="components-download-all">Alle fehlenden Dateien herunterladen</button><form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${(profile.components?.[r.role]===f.id||(!profile.components?.[r.role]&&r.available.length===1))?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}</p>${f.gated&&!f.auth_configured?'<p>Hugging-Face-Zugang unter <a href="#access">Einstellungen → Zugang</a> hinterlegen und Modellfreigabe prüfen.</p>':''}<button type="button" class="secondary" data-component-download="${r.role}:${i}" ${(f.gated&&!f.auth_configured)||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">Lade je eine Datei pro benötigter Komponente. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.</p></form>`:''}<p id="components-message" role="status"></p></section>`;
|
||||
const data=await api('profiles/components?'+new URLSearchParams({model_id:profile.model_id,...(extraRepo?{repo:extraRepo}:{})}));if(!root.isConnected||sequence!==panelSequence)return;
|
||||
el('profile-editor').innerHTML=`<section class="card editor"><div class="card-top"><h2>Komponenten · ${e(profile.name)}</h2><button class="secondary" id="components-close">Schließen</button></div><p>${e(data.message)}</p><details><summary>Andere Komponentenquelle auswählen (ungeprüft)</summary><label>Hugging-Face-Link oder Repository<input id="component-source" placeholder="https://huggingface.co/Organisation/Modell"></label><button id="component-source-open" type="button">Dateien anzeigen</button></details>${data.supported?`<p>Quelle: <a class="link" href="${e(data.source)}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> · ${e(data.license||'Lizenz nicht angegeben')}</p><p>Laufzeit: ${e(data.runtime)} · ${data.manual?'Zuordnung ungeprüft':data.runtime_installed?'installiert':'noch nicht eingerichtet'}</p>${!data.manual?'<button type="button" id="components-download-all">Alle fehlenden Dateien herunterladen</button>':'<p>Nur benötigte Dateien einzeln auswählen; diese Liste ist kein automatischer Downloadplan.</p>'}<form id="components-form">${data.requirements.map(r=>`<section class="card component-card"><h3>${e(r.label)}</h3><label>Datei aus der Bibliothek<select name="${r.role}"><option value="">Noch nicht zugeordnet</option>${r.available.map(f=>`<option value="${f.id}" ${(profile.components?.[r.role]===f.id||(!data.manual&&!profile.components?.[r.role]&&r.available.length===1))?'selected':''}>${e(f.file)} · ${(f.size/1024**3).toFixed(2)} GiB</option>`).join('')}</select></label>${r.candidates.map((f,i)=>`<div class="note"><p data-download-feedback="${r.role}:${i}" role="status"></p><p class="hub-filename">${e(f.name)} · ${(f.size/1024**3).toFixed(2)} GiB · Version ${e(f.revision.slice(0,12))}</p>${f.gated&&!f.auth_configured?'<p>Hugging-Face-Zugang unter <a href="#access">Einstellungen → Zugang</a> hinterlegen und Modellfreigabe prüfen.</p>':''}<button type="button" class="secondary" data-component-download="${r.role}:${i}" ${(f.gated&&!f.auth_configured)||r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'disabled':''}>${r.available.some(x=>x.file===f.name&&x.revision===f.revision)?'Bereits in Bibliothek':'Datei herunterladen'}</button></div>`).join('')||'<p>Keine passende Quelldatei verfügbar.</p>'}</section>`).join('')}<div class="card-actions"><button id="components-save">Zuordnung auf Athena speichern</button><button type="button" class="secondary" id="components-refresh">Bibliothek aktualisieren</button></div><p class="small">Lade je eine Datei pro benötigter Komponente. Downloads erscheinen im Reiter Downloads. Danach Bibliothek aktualisieren, Dateien auswählen und die Zuordnung speichern. Es startet kein Modell.</p></form>`:''}<p id="components-message" role="status"></p></section>`;
|
||||
const missing=data.requirements?.filter(r=>!r.available.length)||[];
|
||||
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{for(const r of missing){const f=r.candidates.find(f=>!f.gated||f.auth_configured);if(!f)throw Error('Keine herunterladbare Datei; HF-Zugang oder Quellversion prüfen für '+r.label);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=error.message;b.disabled=false;}
|
||||
};}
|
||||
el('component-source-open').onclick=()=>{const value=el('component-source').value.trim();if(value)components(profile,value);};
|
||||
el('components-close').onclick=()=>el('profile-editor').replaceChildren();
|
||||
el('components-refresh')?.addEventListener('click',()=>components(profile));
|
||||
el('components-refresh')?.addEventListener('click',()=>components(profile,extraRepo));
|
||||
el('components-form')?.addEventListener('submit',async event=>{event.preventDefault();const button=el('components-save');button.disabled=true;try{await api('profiles/components',{id:profile.id,revision:profile.revision,components:Object.fromEntries(new FormData(event.target))});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Komponenten dem Profil zugeordnet. Kein Modell gestartet.');}catch(error){if(el('components-message'))el('components-message').textContent=error.message;}finally{button.disabled=false;}});
|
||||
root.querySelectorAll('[data-component-download]').forEach(b=>b.onclick=async()=>{const [role,index]=b.dataset.componentDownload.split(':'),f=data.requirements.find(r=>r.role===role).candidates[Number(index)];b.disabled=true;try{await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});if(el('components-message'))el('components-message').textContent='Download gestartet. Fortschritt und Abbruch findest du unter Downloads. Nach Abschluss hier die Bibliothek aktualisieren.';}catch(error){if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;}});
|
||||
root.querySelectorAll('[data-component-download]').forEach(b=>b.onclick=async()=>{const feedback=b.parentElement.querySelector('[data-download-feedback]');feedback.textContent='Download wird angefragt …';const [role,index]=b.dataset.componentDownload.split(':'),f=data.requirements.find(r=>r.role===role).candidates[Number(index)];b.disabled=true;try{await api('catalog/download',{repo:f.repo,filename:f.name,revision:f.revision,kind:profile.kind});feedback.textContent='In Download-Warteschlange aufgenommen.';if(el('components-message'))el('components-message').textContent='Download gestartet. Fortschritt und Abbruch findest du unter Downloads. Nach Abschluss hier die Bibliothek aktualisieren.';}catch(error){feedback.textContent=error.message;feedback.className='warning';if(el('components-message'))el('components-message').textContent=error.message;b.disabled=false;}});
|
||||
}catch(error){message(error.message);el('profile-editor').innerHTML='<section class="card"><p>Komponenten konnten nicht geladen werden. Öffne die Ansicht erneut.</p></section>';}
|
||||
}
|
||||
function gpuEditor(params){
|
||||
|
||||
+39
-7
@@ -5,6 +5,7 @@ import threading
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from catalog import file_role
|
||||
|
||||
SCHEMAS={
|
||||
'chat':{'context':(512,2097152,8192),'slots':(1,16,1),'threads':(1,256,6),'batch':(1,8192,512),'ubatch':(1,8192,128),'temperature':(0,5,.8),'top_p':(0,1,.95),'top_k':(0,1000,40),'mtp_tokens':(1,8,2),'mtp_min_p':(0,1,.05)},
|
||||
@@ -92,7 +93,22 @@ def music_recipe(model):
|
||||
def component_recipe(model):
|
||||
return music_recipe(model) or video_recipe(model) or image_recipe(model)
|
||||
|
||||
MANUAL_ROLES={'manual_text_encoder':'Textencoder (ungeprüft)','manual_vae':'VAE (ungeprüft)','manual_tokenizer':'Tokenizer (ungeprüft)','manual_configuration':'Konfiguration (ungeprüft)','manual_projector':'Projektor (ungeprüft)','manual_auxiliary':'Weitere Komponente (ungeprüft)'}
|
||||
|
||||
def audio_package_recipe(model,source):
|
||||
if not source or not model.get('repo','').startswith('audio-cpp/'):return None
|
||||
for path in sorted(Path(source,'model_specs').glob('*.json')):
|
||||
try:spec=json.loads(path.read_text())
|
||||
except (ValueError,OSError):continue
|
||||
for package in spec.get('packages',[]):
|
||||
files=package.get('files',[])
|
||||
if package.get('target_directory')!=model['repo'].split('/')[-1] or model['file'] not in files:continue
|
||||
extras=[f for f in files if isinstance(f,str) and f!=model['file'] and not f.startswith('/') and '..' not in Path(f).parts]
|
||||
return {f'runtime_file_{i}':dict(label='Laufzeit-Paket: '+f,files=[f]) for i,f in enumerate(extras)} or None
|
||||
return None
|
||||
|
||||
class Profiles:
|
||||
def _recipe(self,model):return component_recipe(model) or getattr(self,'runtime_recipe',lambda m:None)(model)
|
||||
def __init__(self,path,catalog):
|
||||
self.path=Path(path);self.catalog=catalog;self.lock=threading.RLock()
|
||||
self.image_runtime_ready=lambda:False
|
||||
@@ -134,6 +150,7 @@ class Profiles:
|
||||
if p['kind']=='image':
|
||||
for task,repo in PROMPT_ENHANCER_REPOS.items():
|
||||
if p['prompt_enhancer'].get(task)==repo and not self.prompt_enhancer_ready(task):p['blockers'].append('Prompt-Aufwerter '+task.upper()+' fehlt. Unter Bildgenerierung → Aufwerter installieren.')
|
||||
if any(k in MANUAL_ROLES for k in p.get('components',{})):p['blockers'].append('Manuelle Zusatzkomponenten sind ungeprüft und noch nicht an den Worker angebunden.')
|
||||
p['runnable']=not p['blockers'];p['state']='ready' if p['runnable'] else 'configured'
|
||||
return {'profiles':[p for p in rows if p['kind']!='video'],'schemas':{k:v for k,v in SCHEMAS.items() if k!='video'}}
|
||||
def save(self,data):
|
||||
@@ -202,16 +219,20 @@ class Profiles:
|
||||
return {'deleted':old['id']}
|
||||
|
||||
def _component(self,model,role,ident):
|
||||
recipe=component_recipe(model)
|
||||
if role in MANUAL_ROLES:
|
||||
item=self.catalog.entry(ident)
|
||||
if item['kind']!=model['kind'] or item['id']==model['id']:raise ValueError('Andere Zusatzdatei aus derselben Kategorie wählen.')
|
||||
return item
|
||||
recipe=self._recipe(model)
|
||||
if not recipe or role not in recipe:raise ValueError('Für dieses Modell ist kein geprüftes Komponentenrezept hinterlegt.')
|
||||
item=self.catalog.entry(ident)
|
||||
if item['repo']!=recipe[role].get('repo',model['repo']) or item['kind']!=model['kind'] or item['file'] not in recipe[role]['files']:raise ValueError('Die Datei gehört nicht zu dieser Komponente des Modells.')
|
||||
if (video_recipe(model) or music_recipe(model)) and item['revision']!=model['revision']:raise ValueError('Komponente muss dieselbe Quellversion wie das Hauptmodell verwenden.')
|
||||
return item
|
||||
def components(self,model_id):
|
||||
def components(self,model_id,extra_repo=None):
|
||||
model=self.catalog.entry(model_id)
|
||||
if not component_recipe(model):return dict(supported=False,requirements=[],message='Für dieses Modell ist noch kein Komponentenrezept hinterlegt. Keine automatische Zuordnung.')
|
||||
recipe=component_recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
|
||||
if extra_repo or not self._recipe(model):return self.manual_components(model,extra_repo)
|
||||
recipe=self._recipe(model);video=bool(video_recipe(model));source=self.catalog.files(model['repo'],model['revision']) if video or music_recipe(model) else self.catalog.files(next(iter(recipe.values())).get('repo',model['repo']));entries=self.catalog.status()['entries'];requirements=[]
|
||||
for role,info in recipe.items():
|
||||
available=[]
|
||||
for item in entries:
|
||||
@@ -222,16 +243,27 @@ class Profiles:
|
||||
requirements.append(dict(role=role,label=info['label'],available=available,candidates=candidates))
|
||||
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. Der Deck-Musikworker und die Ladypoly-Anbindung fehlen weiterhin; vollständige Komponenten starten kein Modell.')
|
||||
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 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='ComfyUI + ComfyUI-GGUF',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 model['repo']==FLUX_REPO 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'])
|
||||
entries=self.catalog.status()['entries'];requirements=[]
|
||||
for role,label in MANUAL_ROLES.items():
|
||||
match={'manual_text_encoder':('text_encoder',),'manual_vae':('vae',),'manual_projector':('vision_projector','audio_projector'),'manual_configuration':('configuration',),'manual_auxiliary':('auxiliary',),'manual_tokenizer':('configuration','auxiliary')}[role]
|
||||
def matches(name):return name.endswith('.tiktoken') or 'tokenizer' in name.lower() if role=='manual_tokenizer' else file_role(name)['role'] in match
|
||||
candidates=[dict(f,repo=source['repo'],revision=source['revision'],gated=source['gated'],auth_configured=source.get('auth_configured',False)) for f in source['files'] if f['name']!=model['file'] and matches(f['name'])]
|
||||
available=[x for x in entries if x['kind']==model['kind'] and x['id']!=model['id'] and matches(x['file'])]
|
||||
requirements.append(dict(role=role,label=label,available=available,candidates=candidates,optional=True))
|
||||
return dict(supported=True,manual=True,requirements=requirements,source=source['url'],license=source['license'],runtime='Siehe Laufzeiterkennung am Modell',runtime_installed=False,message='Kein vollständiges geprüftes Rezept für diese Auswahl. Diese Dateien sind Hinweise aus Dateinamen und keine bestätigten Pflichtkomponenten. Manuell ausgewählte Dateien werden als ungeprüft gespeichert und schalten keinen Worker frei.')
|
||||
def assign(self,data):
|
||||
if set(data)!={'id','revision','components'} or not isinstance(data['components'],dict):raise ValueError('Ungültige Komponentenzuordnung.')
|
||||
with self.lock:
|
||||
old=next((p for p in self.rows if p['id']==data['id']),None)
|
||||
if not old or old['kind'] not in ('image','video','music') or old['revision']!=data['revision']:raise ValueError('Profil geändert oder nicht gefunden. Ansicht neu laden.')
|
||||
if not old or old['kind'] not in SCHEMAS or old['revision']!=data['revision']:raise ValueError('Profil geändert oder nicht gefunden. Ansicht neu laden.')
|
||||
model=self.catalog.entry(old['model_id'])
|
||||
if set(data['components'])-set(component_recipe(model) or {}):raise ValueError('Unbekannte Komponente für dieses Modell.')
|
||||
components={k:v for k,v in data['components'].items() if v}
|
||||
if set(data['components'])-(set(self._recipe(model) or {})|set(MANUAL_ROLES)):raise ValueError('Unbekannte Komponente für dieses Modell.')
|
||||
components={k:v for k,v in {**old.get('components',{}),**data['components']}.items() if v}
|
||||
for role,ident in components.items():self._component(model,role,ident)
|
||||
updated=dict(old,components=components,revision=old['revision']+1,updated_at=time.time())
|
||||
rows=[updated if p is old else p for p in self.rows]
|
||||
|
||||
@@ -118,6 +118,8 @@ class Server(ThreadingHTTPServer):
|
||||
from audio_cpp_runtime import AudioCppRuntime
|
||||
self.audio_cpp_runtime=AudioCppRuntime(self.catalog.root.parent/'audio-cpp-runtime')
|
||||
self.profiles.music_runtime_ready=lambda:self.audio_cpp_runtime.status()['installed']
|
||||
from profiles import audio_package_recipe
|
||||
self.profiles.runtime_recipe=lambda model:audio_package_recipe(model,self.audio_cpp_runtime.paths()[1])
|
||||
self.ltx_original_runtime=LTXOriginalRuntime(self.catalog.root.parent/'ltx-original-runtime',self.image_runtime)
|
||||
self.video=VideoRuntimes(self.scheduler,stop_gpu_work,self.docker,self.catalog.root.parent/'video',self.catalog,self.profiles,self.image_runtime,original_runtime=self.ltx_original_runtime)
|
||||
self.endpoint.video=self.video
|
||||
@@ -326,7 +328,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
try:return self.respond(self.server.image_tests.image(parse_qs(urlsplit(self.path).query).get('id',[''])[0]),mime='image/png')
|
||||
except (OSError,ValueError):return self.respond({'error':'Bild nicht verfügbar.'},404)
|
||||
if urlsplit(self.path).path == '/api/v1/profiles/components':
|
||||
try:return self.respond(self.server.profiles.components(parse_qs(urlsplit(self.path).query).get('model_id',[''])[0]))
|
||||
try:return self.respond(self.server.profiles.components(parse_qs(urlsplit(self.path).query).get('model_id',[''])[0],parse_qs(urlsplit(self.path).query).get('repo',[None])[0]))
|
||||
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':
|
||||
@@ -372,6 +374,13 @@ class Handler(BaseHTTPRequestHandler):
|
||||
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],runtime_metadata=data.get('runtime_metadata',{})))) for f in data['files']]
|
||||
from profiles import component_recipe
|
||||
from catalog import file_role
|
||||
for f in data['files']:
|
||||
model=dict(repo=data['repo'],file=f['name'],kind=q.get('kind',['chat'])[0])
|
||||
recipe=component_recipe(model) or self.server.profiles.runtime_recipe(model)
|
||||
if recipe:f['execution']['components']=[r['label'] for r in recipe.values()]
|
||||
elif f['execution']['state']!='component':f['execution']['suggested_components']=[x['name'] for x in data['files'] if file_role(x['name'])['role'] in ('text_encoder','vae','vision_projector','audio_projector')][:12]
|
||||
return self.respond(data)
|
||||
if path.path=='/api/v1/catalog':
|
||||
data=self.server.catalog.status()
|
||||
|
||||
@@ -24,3 +24,24 @@ class MusicComponentsTests(unittest.TestCase):
|
||||
for info in YUE2_COMPONENTS.values():
|
||||
for filename in info['files']:self.assertFalse(file_role(filename)['profile_eligible'])
|
||||
self.assertIsNone(music_recipe(dict(kind='music',repo='other/Yue2',file='yue2-3b-bf16.gguf')))
|
||||
def test_manual_setup_available_in_every_category_and_preserves_recipe(self):
|
||||
for kind in ('chat','image','audio','stt','music','voice','video'):
|
||||
model=dict(id='main',kind=kind,repo='unknown/model',file='weights.gguf',revision='a'*40)
|
||||
component=dict(id='aux',kind=kind,repo='external/repo',file='vae.safetensors',revision='b'*40,size=1)
|
||||
c=Mock();c.entry.side_effect=lambda id: {'main':model,'aux':component}[id]
|
||||
c.status.return_value=dict(entries=[component]);c.files.return_value=dict(repo='unknown/model',revision='a'*40,files=[],gated=False,url='https://huggingface.co/unknown/model',license=None)
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Profiles(Path(d)/'profiles.json',c);p.rows=[dict(id='p',kind=kind,revision=1,name='Test',model_id='main',parameters={})]
|
||||
self.assertTrue(p.components('main')['manual'])
|
||||
p.assign(dict(id='p',revision=1,components={'manual_vae':'aux'}))
|
||||
self.assertEqual(p.rows[0]['components']['manual_vae'],'aux')
|
||||
self.assertTrue(p.references('aux'))
|
||||
with self.assertRaises(ValueError):p.assign(dict(id='p',revision=2,components={'arbitrary':'aux'}))
|
||||
def test_audio_runtime_package_definition_matches_exact_repo_and_model(self):
|
||||
import json
|
||||
from profiles import audio_package_recipe
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
Path(d,'model_specs').mkdir();Path(d,'model_specs/test.json').write_text(json.dumps(dict(packages=[dict(target_directory='Example-GGUF',files=['model.gguf','config.json'])])))
|
||||
model=dict(repo='audio-cpp/Example-GGUF',file='model.gguf')
|
||||
self.assertEqual(audio_package_recipe(model,d)['runtime_file_0']['files'],['config.json'])
|
||||
self.assertIsNone(audio_package_recipe(dict(model,repo='other/Example-GGUF'),d))
|
||||
|
||||
Reference in New Issue
Block a user