Files
Athena-Deck/separator-ui.js
T

33 lines
8.2 KiB
JavaScript

window.SeparatorUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
let section='discover';
async function api(path,data){const response=await fetch('/api/v1/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const value=await response.json();if(!response.ok)throw Error(value.error||'Anfrage fehlgeschlagen');return value;}
function sources(m){return `<p>Quelle: ${e(m.source?.publisher||'Nicht belegt')}</p><details><summary>Originalquellen und Prüfsummen</summary>${(m.source?.files||[]).map(f=>`<p class="hub-filename">${e(f.file)}<br>${f.urls.map(u=>`<a href="${e(u)}" target="_blank" rel="noopener">Downloadquelle</a>`).join(' · ')}<br><code>${e(f.sha256)}</code></p>`).join('')}</details>`;}
function rows(models,library){const list=models.filter(m=>!library||m.installed);return list.length?`<div class="detail-list">${list.map(m=>`<details class="detail-row"><summary class="detail-summary"><strong>${e(m.name)}</strong><span>${m.installed?'Bereit':'Nicht heruntergeladen'}</span><span>${m.source?.files?.length||0} Dateien</span></summary><div class="detail-body"><p class="hub-filename">${e(m.id)}</p>${sources(m)}<p>Vollständiges Modellpaket · einschließlich Konfiguration und benötigter Teilmodelle. Die Laufzeit wird wiederverwendet.</p>${m.installed?'<a class="link" href="#home">In der Steuerung Audio-Trennung starten →</a>':`<button data-sep-model="${e(m.id)}">Modellpaket herunterladen</button>`}</div></details>`).join('')}</div>`:'<section class="empty"><h2>Noch keine Modellpakete</h2><p>Unter Entdecken ein geprüftes Modellpaket herunterladen.</p></section>';}
function render(tool=false,force=false){if(!force&&document.querySelector('#separator-panel')?.dataset.tool===String(tool))return;
const tabs=[['discover','Entdecken'],['library','Bibliothek'],['downloads','Downloads'],['test','Testen']];
document.querySelector('#view').innerHTML=`<div id="separator-panel" data-tool="${tool}"><div class="kicker">${tool?'ATHENA / MODELLVERWALTUNG':'EINSTELLUNGEN / LAUFZEITEN'}</div><h1>${tool?'Audio-Trennung':'Audio Separator'}</h1><p>${tool?'Gesang, Schlagzeug, Bass, Gitarre, Klavier oder die übrige Begleitung aus einer Aufnahme herauslösen.':'Native CUDA-Laufzeit für BS-Roformer, MelBand und Demucs. Einmal installieren und für passende Modellpakete wiederverwenden.'}</p>${tool?`<div class="studio-tabs" role="tablist" aria-label="Modellverwaltung">${tabs.map(([id,label])=>`<button role="tab" aria-selected="${section===id}" class="${section===id?'chosen':'secondary'}" data-sep-tab="${id}">${label}</button>`).join('')}</div>`:''}<p id="sep-message" role="status"></p><div role="tabpanel"><div id="sep-content"></div></div></div>`;
const root=document.querySelector('#separator-panel'),msg=root.querySelector('#sep-message'),content=root.querySelector('#sep-content');let pending=false,signature='';
async function action(path,data){pending=true;try{await api(path,data);msg.textContent='Aktion gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}}
function bind(s,t){
root.querySelectorAll('[data-sep-model]').forEach(b=>{b.disabled=pending||s.job?.state==='running'||!s.installed;b.onclick=()=>action('separator-runtime/download',{model:b.dataset.sepModel});});
const install=root.querySelector('#sep-install');if(install){install.disabled=pending||s.installed||s.job?.state==='running';install.onclick=()=>action('separator-runtime/install',{});}
const cancel=root.querySelector('#sep-cancel');if(cancel){cancel.disabled=s.job?.state!=='running';cancel.onclick=()=>action('separator-runtime/cancel',{});}
const stop=root.querySelector('#sep-test-cancel');if(stop){stop.disabled=t.job?.state!=='running';stop.onclick=()=>action('separator-tests/cancel',{});}
const form=root.querySelector('#sep-test');if(form){form.querySelector('button').disabled=pending||t.job?.state==='running'||!s.models.some(m=>m.installed);form.onsubmit=async event=>{event.preventDefault();pending=true;msg.textContent='Audio wird hochgeladen …';try{const response=await fetch('/api/v1/separator-tests/start',{method:'POST',headers:{'X-Athena-Deck':'1'},body:new FormData(form)}),value=await response.json();if(!response.ok)throw Error(value.error);msg.textContent='Trenntest gestartet.';}catch(error){msg.textContent=error.message;}finally{pending=false;signature='';poll();}};}
}
async function poll(){if(!root.isConnected)return;try{const [s,t]=await Promise.all([api('separator-runtime'),api('separator-tests')]);if(!root.isConnected)return;
const key=JSON.stringify([s,t,pending]);if(key!==signature&& (!root.querySelector('#sep-test')?.contains(document.activeElement)||t.job?.state==='running'||root.dataset.jobState!==String(t.job?.state))){signature=key;root.dataset.jobState=String(t.job?.state);
if(!tool){content.innerHTML=`<section class="card"><h2>Laufzeit</h2><p><strong>${s.installed?'Installiert':'Noch nicht installiert'}</strong> · Version ${e(s.version)}</p><button id="sep-install">Laufzeit installieren</button><button class="secondary" id="sep-cancel">Einrichtung abbrechen</button><p>Mindestens 15 GiB frei. Keine Änderung an Host-Treibern oder alten Diensten.</p><a class="link" href="#separation">Audio-Trennung öffnen →</a><p>${e(s.job?.phase||'')}</p>${s.job?.state==='running'?'<progress aria-label="Einrichtung läuft"></progress>':''}<details><summary>Installationsausgabe</summary><pre id="sep-log" style="max-height:240px;overflow:auto;white-space:pre-wrap"></pre></details></section>`;
const log=await api('separator-runtime/log');if(root.isConnected)root.querySelector('#sep-log').textContent=log.text;
}else if(section==='discover'||section==='library'){content.innerHTML=`${section==='discover'?'<p>Geprüfte Pakete mit allen benötigten Zusatzdateien. Downloads prüfen die Originalquellen per SHA-256.</p>':''}${!s.installed?'<p class="warning">Laufzeit fehlt. <a href="#separator-runtime">Audio Separator installieren →</a></p>':''}${rows(s.models,section==='library')}`;
}else if(section==='downloads'){content.innerHTML=s.job?`<section class="card"><span class="pill">${e(s.job.state)}</span><h2>Einrichtungsauftrag</h2><p>${e(s.job.phase)}</p>${s.job.state==='running'?'<progress aria-label="Download läuft"></progress><button id="sep-cancel" class="secondary">Abbrechen</button>':''}<p>Abgeschlossene Modellpakete findest du in der Bibliothek.</p></section>`:'<section class="empty"><h2>Keine Downloads</h2><p>Unter Entdecken ein Modellpaket auswählen.</p></section>';
}else{const j=t.job;content.innerHTML=`<section class="card"><h2>Audio-Trennung testen</h2><p>Kurzer PCM-WAV-Test: 16 Bit, Mono/Stereo, bis 30 Sekunden und 8 MiB. Für längere Dateien die Weboberfläche verwenden.</p><p><a class="link" href="${e(location.protocol+'//'+location.hostname+':8128/')}" target="_blank" rel="noopener">Audio-Trennoberfläche öffnen →</a> · <a href="#home">Audio-Trennung in der Steuerung starten →</a></p><p class="small">Gewichte laden bei Auftrag und werden anschließend entladen. Fremde Dienste bleiben unberührt.</p><form id="sep-test"><label>Modellpaket<select name="model">${s.models.filter(m=>m.installed).map(m=>`<option value="${e(m.id)}">${e(m.name)}</option>`).join('')}</select></label><label>Audiodatei<input name="file" type="file" accept=".wav,audio/wav" required></label><button>Audio trennen</button><button type="button" class="secondary" id="sep-test-cancel">Abbrechen</button></form><div role="status">${j?`<p>${e(j.state)} · ${e(j.phase)} ${j.elapsed_seconds?e(j.elapsed_seconds)+' s':''}</p>${j.state==='running'?'<progress aria-label="Audio wird getrennt"></progress>':''}`:''}</div>${j?.state==='complete'?j.files.map(f=>`<p>${e(f.name)}</p><audio controls src="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}"></audio><p><a href="/api/v1/separator-tests/audio?id=${e(j.id)}&index=${f.index}" download="${e(f.name)}">Spur herunterladen</a></p>`).join(''):''}</section>`;}
}
bind(s,t);
}catch(error){if(root.isConnected)msg.textContent=error.message;}clearTimeout(root.pollTimer);if(root.isConnected)root.pollTimer=setTimeout(poll,3000);}
root.querySelectorAll('[data-sep-tab]').forEach(b=>b.onclick=()=>{section=b.dataset.sepTab;render(tool,true);});poll();
}
return {render};
})();