Files
Athena-Deck/image-test-ui.js
T

48 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
window.ImageTestUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
async function api(path,data){const r=await fetch('/api/v1/'+path,data?{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)}:{});const v=await r.json();if(!r.ok)throw Error(v.error||'Anfrage fehlgeschlagen');return v;}
function html(readonly=false){return `<section id="image-test"><div class="section-heading"><div><h2>${readonly?'Bildlaufzeit':'Bildmodell testen'}</h2><p>Ein Bild mit deinem gespeicherten Profil erzeugen. Der eigene Worker beendet sich nach dem Auftrag.</p></div></div>${readonly?'':`<form id="image-test-form" class="card"><label>Bildprofil<select name="profile_id" id="image-test-profile" required><option value="">Profile werden geladen …</option></select></label><p id="image-profile-info"></p><label>Prompt<textarea name="prompt" maxlength="4000" rows="5" required placeholder="Beschreibe das gewünschte Bild …"></textarea></label><label id="image-reference-label">Referenzbilder (optional)<input id="image-reference-files" name="image[]" type="file" accept="image/png,image/jpeg,image/webp" multiple></label><p id="image-reference-info" class="small"></p><input type="hidden" name="skip_enhancer" id="image-skip-enhancer" value="0"><button type="button" class="secondary" id="image-preview-enhancer">Prompt-Aufbereitung ansehen</button><button id="image-generate" disabled>Bild generieren</button><div id="image-enhancer-preview" role="status"></div><p class="small">Auflösung, Schritte, Guidance und Seed stammen aus dem Profil. Maximal 1024 × 1024 Pixel. Beide GPUs müssen frei sein; Deck stoppt keine produktiven Modelle. Der erste Start kann einige Minuten dauern.</p></form>`}<p class="note">Textencoder: RTX 3060 · Diffusionsmodell und VAE: RTX 5080. CPU/RAM werden weiterhin zum Laden und Auslagern benötigt. GPU-Last entsteht nicht während aller Verarbeitungsschritte. <a class="link" href="#hardware">Aktuelle Hardware-Messwerte →</a></p><p id="image-test-message" role="status"></p><section id="image-test-state" class="card"></section><section id="image-test-result"></section></section>`;}
function bind(readonly=false){
const root=document.querySelector('#image-test'),el=id=>root.querySelector('#'+id);let profiles=[],status=null,lastJob='',timer,submitting=false,previewId=null;
const message=t=>{if(root.isConnected)el('image-test-message').textContent=t;};
function selection(){if(readonly)return;const p=profiles.find(p=>p.id===el('image-test-profile').value),max=p?.capabilities?.reference_images||0;el('image-profile-info').textContent=p?`${p.parameters.width} × ${p.parameters.height} · ${p.parameters.steps} Schritte · Guidance ${p.parameters.guidance} · Seed ${p.parameters.seed}${p.blockers.length?' · '+p.blockers.join(' '):''}`:'';el('image-reference-label').hidden=!max;el('image-reference-files').disabled=!max;el('image-reference-info').textContent=max?`Dieses Profil erlaubt bis zu ${max} Referenzbilder. Reihenfolge: wie ausgewählt; PNG/JPEG/WebP, je höchstens 10 MiB.`:'Dieses Profil unterstützt keine Referenzbilder.';el('image-generate').disabled=submitting||!p?.runnable||!status?.runtime_installed||status?.job?.state==='running';el('image-preview-enhancer').disabled=submitting||!p?.prompt_enhancer?.[el('image-reference-files').files.length?'i2i':'t2i'];}
async function preview(){
const button=el('image-preview-enhancer'),area=el('image-enhancer-preview');button.disabled=true;area.textContent='Prompt-Aufwerter startet; Bildmodell wartet.';
try{
const form=new FormData();form.set('profile_id',el('image-test-profile').value);form.set('prompt',el('image-test-form').elements.prompt.value);
for(const file of el('image-reference-files').files)form.append('image[]',file);
const response=await fetch('/api/v1/prompt-enhancers/preview',{method:'POST',headers:{'X-Athena-Deck':'1'},body:form});const job=await response.json();
if(!response.ok)throw Error(job.error||'Vorschau konnte nicht starten.');previewId=job.id;
async function poll(){
if(!root.isConnected||previewId!==job.id)return;
const state=(await api('prompt-enhancers')).preview;if(!state||state.id!==job.id)return;
if(state.state==='running'){area.textContent=state.phase;setTimeout(poll,2000);return;}
if(state.state!=='complete')throw Error(state.phase||'Aufbereitung fehlgeschlagen.');
area.innerHTML=`<p><strong>Vorschlag</strong> · Seitenverhältnis: ${e(state.result.wh_ratio||'kein Vorschlag')}</p><textarea id="image-enhanced-text" rows="8" maxlength="4000"></textarea><p><button type="button" id="image-use-enhanced">Vorschlag übernehmen</button><button type="button" class="secondary" id="image-use-original">Original behalten</button></p>`;
area.querySelector('textarea').value=state.result.prompt;
area.querySelector('#image-use-enhanced').onclick=()=>{el('image-test-form').elements.prompt.value=area.querySelector('textarea').value;el('image-skip-enhancer').value='1';area.textContent='Überarbeiteter Prompt übernommen. Bildgenerierung bereitet ihn nicht erneut auf.';};
area.querySelector('#image-use-original').onclick=()=>{el('image-skip-enhancer').value='1';area.textContent='Originalprompt bleibt. Für diesen Bildtest wird die Aufbereitung übersprungen.';};
}
poll().catch(error=>{area.textContent=error.message;});
}catch(error){area.textContent=error.message;}finally{button.disabled=false;}
}
async function refresh(){try{status=await api('image-tests');if(!root.isConnected)return;const j=status.job,signature=JSON.stringify(j);if(signature!==lastJob){lastJob=signature;el('image-test-state').innerHTML=j?`<span class="pill">${e({running:'LÄUFT',complete:'FERTIG',failed:'FEHLGESCHLAGEN',cancelled:'ABGEBROCHEN',interrupted:'UNTERBROCHEN'}[j.state]||j.state)}</span><h3>${e(j.profile_name)}</h3><p>${e(j.phase)}</p><p class="small">${e(j.gpu)}${j.encoder_gpu?' · Textencoder: '+e(j.encoder_gpu):''} · Seed ${e(j.seed)}</p>${j.state==='running'?'<button class="secondary" id="image-cancel">Bildtest abbrechen</button>':''}`:`<h3>${status.runtime_installed?'Bildlaufzeit ist installiert':'Bildlaufzeit noch nicht installiert'}</h3><p>Noch kein Bildtest gestartet.</p>`;
el('image-test-result').innerHTML=j?.state==='complete'?`<figure class="card"><img class="generated-image" src="/api/v1/image-tests/image?id=${encodeURIComponent(j.id)}" alt="Ergebnis des Bildtests"><figcaption><a class="link" href="/api/v1/image-tests/image?id=${encodeURIComponent(j.id)}" download="athena-${e(j.id)}.png">PNG herunterladen</a></figcaption></figure>`:'';
el('image-cancel')?.addEventListener('click',async event=>{event.target.disabled=true;try{await api('image-tests/cancel',{});message('Abbruch angefordert; der eigene Bildworker wird beendet.');}catch(error){message(error.message);}});
}selection();}catch(error){message(error.message);}finally{if(root.isConnected)timer=setTimeout(refresh,2000);}}
if(!readonly){api('profiles').then(v=>{if(!root.isConnected)return;profiles=v.profiles.filter(p=>p.kind==='image');el('image-test-profile').innerHTML=profiles.map(p=>`<option value="${p.id}">${e(p.name)}${p.runnable?'':' · Komponenten / Laufzeit prüfen'}</option>`).join('')||'<option value="">Zuerst ein Bildprofil anlegen</option>';selection();}).catch(error=>message(error.message));const invalidate=()=>{previewId=null;el('image-skip-enhancer').value='0';el('image-enhancer-preview').replaceChildren();};el('image-test-profile').onchange=()=>{el('image-reference-files').value='';invalidate();selection();};
el('image-reference-files').onchange=()=>{invalidate();selection();};
el('image-test-form').elements.prompt.oninput=invalidate;
el('image-preview-enhancer').onclick=preview;
el('image-test-form').onsubmit=async event=>{event.preventDefault();submitting=true;selection();try{const form=new FormData(event.target),p=profiles.find(p=>p.id===el('image-test-profile').value),files=[...el('image-reference-files').files],max=p?.capabilities?.reference_images||0;if(files.length>max)throw Error(`Dieses Profil erlaubt höchstens ${max} Referenzbilder.`);if(files.some(f=>f.size>10*1024*1024))throw Error('Jedes Referenzbild darf höchstens 10 MiB groß sein.');form.delete('image[]');for(const file of files)form.append('image[]',file);const r=await fetch('/api/v1/image-tests/start',{method:'POST',headers:{'X-Athena-Deck':'1'},body:form});const result=await r.json();if(!r.ok)throw Error(result.error||'Bildtest fehlgeschlagen.');message('Bildtest gestartet.');}catch(error){message(error.message);}finally{submitting=false;selection();}};
}
refresh();
}
function bindInstaller(){
const root=document.querySelector('#image-runtime-install');let pending=false;
async function refresh(){try{const s=await api('image-runtime');if(!root.isConnected)return;const running=s.job?.state==='running';root.innerHTML=`<p><strong>${s.installed?'Installiert · '+e(s.origin):'Noch nicht installiert'}</strong></p><p class="small">ComfyUI ${e(s.comfy_revision.slice(0,12))} · GGUF ${e(s.gguf_revision.slice(0,12))}</p><p>${e(s.job?.phase||'Installation richtet eine eigene Python-Umgebung mit ComfyUI, GGUF-Unterstützung und CUDA-PyTorch ein. Mindestens 25 GiB freier Speicher und Internetzugang erforderlich.')}</p><button id="image-runtime-action" ${pending||s.installed?'disabled':''}>${s.installed?'Bereits installiert':running?'Installation abbrechen':'Bildlaufzeit installieren'}</button><p class="small">Systemvoraussetzungen: Python mit venv, Git und kompatibler NVIDIA-Treiber. Die Installation ändert keine Host-Pakete oder anderen Dienste.</p>`;root.querySelector('button').onclick=async()=>{pending=true;root.querySelector('button').disabled=true;try{await api('image-runtime/'+(running?'cancel':'install'),{});}catch(error){alert(error.message);}finally{pending=false;}};}catch(error){if(root.isConnected)root.textContent=error.message;}finally{if(root.isConnected)setTimeout(refresh,2000);}}
refresh();
}
return {html,bind,bindInstaller};
})();