Files

23 lines
6.2 KiB
JavaScript

window.TTSUI=(()=>{
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===undefined)?{}:{method:'POST',headers:{'Content-Type':'application/json','X-Athena-Deck':'1'},body:JSON.stringify(data)});const x=await r.json();if(!r.ok)throw Error(x.error||'TTS-Anfrage fehlgeschlagen');return x;}
function html(setup=false){return `<section id="tts-panel" class="card"><h2>${setup?'Qwen3-TTS einrichten':'Sprache ausprobieren'}</h2><p id="tts-status" role="status"></p><p id="tts-error" role="alert"></p>${setup?'<p>Die Einrichtung installiert eine eigene CUDA-Laufzeit und das offizielle Qwen3-TTS 1.7B CustomVoice mit eingebauten Stimmen. Dafür werden mehrere GB heruntergeladen. MLX-Dateien sind für Apple-Geräte; vorhandene Dateien werden nicht gelöscht.</p><button id="tts-install">TTS-Laufzeit und Modell einrichten</button><button class="secondary" id="tts-install-cancel">Einrichtung abbrechen</button><h3>Vorhandenes TTS-Profil verbinden</h3><p>Ersetzt im ausgewählten Profil die Modellzuordnung durch die eingerichtete CUDA-Variante. Der Profilname bleibt erhalten.</p><select id="tts-profile"></select><button id="tts-assign">CUDA-Modell diesem Profil zuordnen</button><h3>Speicherverhalten</h3><p>Automatisch hält das Modell nach der ersten Anfrage geladen. Immer bereit lädt das gewählte Profil im LLM-Betrieb vor, soweit Ressourcen frei sind. Nach jeder Anfrage entladen gibt Speicher sofort frei.</p><label>Verhalten<select id="tts-policy"><option value="auto">Automatisch</option><option value="warm">Immer bereit</option><option value="per_request">Nach jeder Anfrage entladen</option></select></label><label>Profil für „Immer bereit“<select id="tts-warm-profile"></select></label><button id="tts-policy-save">Speicherverhalten speichern</button><p id="tts-policy-state" role="status"></p>':'<form id="tts-form"><label>Profil<select id="tts-profile" required></select></label><div class="form-grid"><label>Stimme<select id="tts-speaker"></select></label><label>Sprache<select id="tts-language"></select></label></div><label>Text<textarea id="tts-text" rows="5" maxlength="1000" required placeholder="Was soll vorgelesen werden?"></textarea></label><button id="tts-generate">Sprache erzeugen</button><button type="button" id="tts-cancel" class="secondary">Abbrechen</button></form><div id="tts-result"></div><p>Ausführung auf der RTX 3060. Andere Deck-Modellaufträge werden koordiniert; fremde Dienste werden nicht gestoppt. Text wird nicht dauerhaft gespeichert. Ergebnisdateien bleiben auf Athena.</p>'}</section>`;}
function bind(setup=false){const root=document.querySelector('#tts-panel');if(!root)return;const el=id=>root.querySelector('#'+id);let state,profiles=[],busy=false,lastResult='';const error=x=>{if(root.isConnected)el('tts-error').textContent=x;};
async function refresh(){try{state=await api('tts');if(!root.isConnected)return;const rt=state.runtime,j=state.job;el('tts-status').textContent=setup?(rt.job?.phase||(rt.installed?'TTS ist eingerichtet.':'Noch nicht eingerichtet.')):(j?.phase||'Profil wählen und Text eingeben.')+` · RTX 3060 ${state.loaded?'geladen':'nicht geladen'}`;
if(setup){el('tts-install').disabled=busy||rt.installed||rt.job?.state==='running';el('tts-install-cancel').disabled=rt.job?.state!=='running';el('tts-assign').disabled=busy||!rt.installed||!profiles.length;}
else{el('tts-generate').disabled=busy||j?.state==='running'||!profiles.length||!rt.installed;el('tts-cancel').disabled=j?.state!=='running';if(j?.state==='complete'&&lastResult!==j.id){lastResult=j.id;el('tts-result').innerHTML=`<h3>Ergebnis</h3><audio controls src="/api/v1/tts/audio?id=${e(j.id)}"></audio><p><a class="link" download="athena-sprache.wav" href="/api/v1/tts/audio?id=${e(j.id)}">WAV herunterladen</a></p>`;}}
}catch(err){error(err.message);}finally{if(root.isConnected)setTimeout(refresh,2000);}}
async function load(){const p=await api('profiles');if(!root.isConnected)return;profiles=p.profiles.filter(p=>p.kind==='audio'&&(setup||p.runnable));el('tts-profile').innerHTML=profiles.map(p=>`<option value="${e(p.id)}">${e(p.name)}</option>`).join('')||'<option value="">Zuerst ein TTS-Profil anlegen und unter Einrichten verbinden</option>';}
async function action(path,data={}){busy=true;error('');try{await api('tts/'+path,data);if(path==='assign'){await load();error('Profil verbunden. Jetzt den Reiter Testen öffnen.');}}catch(err){error(err.message);}finally{busy=false;}}
if(setup){el('tts-install').onclick=()=>action('install');el('tts-install-cancel').onclick=()=>action('install-cancel');el('tts-assign').onclick=()=>{const p=profiles.find(p=>p.id===el('tts-profile').value);if(p)action('assign',{id:p.id,revision:p.revision});};}
else{api('tts').then(s=>{if(!root.isConnected)return;el('tts-speaker').innerHTML=s.runtime.speakers.map(v=>`<option ${v==='Ryan'?'selected':''}>${e(v)}</option>`).join('');el('tts-language').innerHTML=s.runtime.languages.map(v=>`<option ${v==='German'?'selected':''}>${e(v)}</option>`).join('');}).catch(x=>error(x.message));el('tts-form').onsubmit=ev=>{ev.preventDefault();action('start',{profile_id:el('tts-profile').value,text:el('tts-text').value,speaker:el('tts-speaker').value,language:el('tts-language').value});};el('tts-cancel').onclick=()=>action('cancel');}
if(setup){
const policy=el('tts-policy'),warm=el('tts-warm-profile'),save=el('tts-policy-save'),status=el('tts-policy-state');
Promise.all([api('audio-policy'),api('profiles')]).then(([current,rows])=>{if(!root.isConnected)return;warm.innerHTML=rows.profiles.filter(p=>p.kind==='audio'&&p.runnable).map(p=>`<option value="${e(p.id)}">${e(p.name)}</option>`).join('');const selected=current.settings.tts;policy.value=selected.mode;if(selected.profile_id)warm.value=selected.profile_id;status.textContent=current.errors.tts||'';}).catch(err=>{if(root.isConnected)status.textContent=err.message;});
save.onclick=async()=>{save.disabled=true;try{const updated=await api('audio-policy',{kind:'tts',mode:policy.value,profile_id:warm.value||null});status.textContent=updated.errors.tts||'Gespeichert.';}catch(err){status.textContent=err.message;}finally{save.disabled=false;}};
}
load().catch(x=>error(x.message));refresh();
}
return {html,bind};
})();