Add optional per-image-profile Qwen prompt enhancers

This commit is contained in:
Mikei386
2026-09-29 21:16:40 +02:00
parent 7ba17308f5
commit c5a028cba9
13 changed files with 363 additions and 19 deletions
+17 -1
View File
@@ -7,14 +7,30 @@ 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>${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'].includes(kind)?`<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(['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'].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').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;}});
root.querySelectorAll('[data-components]').forEach(b=>b.onclick=()=>components(rows.find(p=>p.id===b.dataset.components)));
root.querySelectorAll('[data-enhancers]').forEach(b=>b.onclick=()=>enhancers(rows.find(p=>p.id===b.dataset.enhancers)));
root.querySelectorAll('[data-edit]').forEach(b=>b.onclick=()=>editor(rows.find(p=>p.id===b.dataset.edit)));
root.querySelectorAll('[data-copy]').forEach(b=>b.onclick=()=>{const p=rows.find(p=>p.id===b.dataset.copy);editor({...p,id:null,revision:0,name:p.name.slice(0,55)+'-kopie'});});
}catch(error){message(error.message);}}
async function enhancers(profile){
const sequence=++panelSequence,selected=profile.prompt_enhancer||{t2i:null,i2i:null,device:'auto'};
el('profile-editor').innerHTML=`<section class="card editor"><h2>Prompt-Aufwerter · ${e(profile.name)}</h2><p>Optional pro Bildprofil. Text-zu-Bild und Bearbeitung mit Referenzbildern verwenden getrennte offizielle Qwen-Modelle. Andere Bildfamilien können sie nur nach eigener Prüfung sinnvoll nutzen.</p><form id="enhancer-form"><div class="form-grid"><label>Text-zu-Bild<select name="t2i"><option value="">Aus · Originalprompt verwenden</option><option value="Qwen/Qwen-Image-2.1-PE-T2I" ${selected.t2i?'selected':''}>Qwen Image 2.1 PE-T2I</option></select></label><label>Bildbearbeitung mit Referenzen<select name="i2i"><option value="">Aus · Originalprompt verwenden</option><option value="Qwen/Qwen-Image-2.1-PE-I2I" ${selected.i2i?'selected':''}>Qwen Image 2.1 PE-I2I</option></select></label><label>Ausführungsgerät<select name="device">${[['auto','Automatisch · 5080, dann 3060, nötigenfalls RAM'],['5080','RTX 5080 · nötigenfalls RAM'],['3060','RTX 3060 · nötigenfalls RAM'],['cpu','Nur CPU · sehr langsam']].map(([v,t])=>`<option value="${v}" ${selected.device===v?'selected':''}>${t}</option>`).join('')}</select></label></div><p>Der Aufwerter wird vor der Bildpipeline geladen und danach vollständig entladen. Beide PE-Modelle benötigen je etwa 18,8 GB Download. Die Wahl einer einzelnen GPU garantiert keinen rein dortigen Betrieb; CPU-Auslagerung kann erforderlich sein. Die Bildauflösung bleibt im Profil gespeichert; ein Seitenverhältnis aus der PE-Antwort ist nur ein Vorschlag.</p><button>Profil-Einstellung speichern</button><button type="button" class="secondary" id="enhancer-close">Schließen</button></form><h3>Offizielle Modelle installieren</h3><div id="enhancer-models"></div><p id="enhancer-message" role="status"></p></section>`;
const note=el('enhancer-message');el('enhancer-close').onclick=()=>el('profile-editor').replaceChildren();
el('enhancer-form').onsubmit=async event=>{event.preventDefault();const form=event.target,values=new FormData(form),button=form.querySelector('button');button.disabled=true;try{await api('profiles/prompt-enhancer',{id:profile.id,revision:profile.revision,prompt_enhancer:{t2i:values.get('t2i')||null,i2i:values.get('i2i')||null,device:values.get('device')}});if(!root.isConnected)return;el('profile-editor').replaceChildren();await load();message('Prompt-Aufwerter im Bildprofil gespeichert.');}catch(error){note.textContent=error.message;}finally{button.disabled=false;}};
async function refresh(){
try{const state=await api('prompt-enhancers');if(!root.isConnected||sequence!==panelSequence)return;
el('enhancer-models').innerHTML=['t2i','i2i'].map(task=>{const item=state.models[task];return `<p><strong>${task.toUpperCase()}</strong> · ${item.installed?'Installiert · '+e(item.revision?.slice(0,12)):'Nicht installiert'} · <a class="link" href="https://huggingface.co/${item.repo}" target="_blank" rel="noopener noreferrer">Modellkarte und Lizenz ↗</a> <button type="button" class="secondary" data-install="${task}" ${item.installed||state.job?.state==='running'?'disabled':''}>Herunterladen</button></p>`;}).join('')+`<p>${e(state.job?.phase||'Kein Download läuft.')}</p>${state.job?.state==='running'?'<button type="button" class="secondary" id="enhancer-cancel">Download abbrechen</button>':''}`;
el('enhancer-models').querySelectorAll('[data-install]').forEach(button=>button.onclick=async()=>{button.disabled=true;try{await api('prompt-enhancers/install',{task:button.dataset.install});note.textContent='Download gestartet. Die Bildmodelle bleiben unberührt.';}catch(error){note.textContent=error.message;}});
el('enhancer-cancel')?.addEventListener('click',async()=>{try{await api('prompt-enhancers/cancel',{});}catch(error){note.textContent=error.message;}});
}catch(error){if(root.isConnected&&sequence===panelSequence)note.textContent=error.message;}
finally{if(root.isConnected&&sequence===panelSequence)setTimeout(refresh,3000);}
}
refresh();el('profile-editor').scrollIntoView({behavior:'smooth',block:'start'});
}
async function components(profile){
const sequence=++panelSequence;
el('profile-editor').innerHTML='<section class="card editor"><p role="status">Komponenten werden geprüft …</p></section>';