Files
Athena-Deck/voice-ui.js
T

13 lines
4.1 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.VoiceUI=(()=>{
const e=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
function html(){return `<section id="voice-panel" class="card"><h2>Stimme ausprobieren · VoxCPM2</h2><p>Text erzeugt Sprache. Mit optionaler WAV-Referenz übernimmt VoxCPM2 deren Stimme. Nur eingerichtete VoxCPM2-Profile stehen zur Auswahl.</p><p id="voice-status" role="status"></p><progress id="voice-progress" hidden aria-label="Stimmgenerierung"></progress><p id="voice-error" role="alert"></p><form id="voice-form"><label>Profil<select name="profile_id" id="voice-profile" required></select></label><label>Text<textarea name="text" rows="5" maxlength="1000" required></textarea></label><label>Referenzstimme (optional, PCM-16-WAV, 1–30 Sekunden, bis 8 MiB)<input name="file" type="file" accept="audio/wav,.wav"></label><div class="form-grid"><label>GPU<select name="device"><option value="5080">RTX 5080</option><option value="3060">RTX 3060</option></select></label><label>Guidance<input name="cfg" type="number" min="0.5" max="5" step="0.1" value="2"></label><label>Schritte<input name="steps" type="number" min="1" max="50" value="10"></label><label>Seed<input name="seed" type="number" min="0" max="2147483647" value="42"></label></div><button id="voice-start">Stimme erzeugen</button><button id="voice-cancel" type="button" class="secondary" disabled>Abbrechen</button></form><div id="voice-result"></div><p class="small">Im LLM-Modus verfügbar. Deck entlädt sein LLM vor der Stimmgenerierung und lädt es bei der nächsten Chat-Anfrage wieder. VoxCPM2 wird nach dem Auftrag entladen. Referenzdatei wird nach dem Auftrag gelöscht; Text wird nicht gespeichert.</p></section>`;}
function bind(){const root=document.querySelector('#voice-panel');if(!root)return;const el=id=>root.querySelector('#'+id);let busy=false,lastResult='',ready=false;
async function api(path='',body){const r=await fetch('/api/v1/voice'+path,body===undefined?{}:{method:'POST',headers:body instanceof FormData?{'X-Athena-Deck':'1'}:{'X-Athena-Deck':'1','Content-Type':'application/json'},body:body instanceof FormData?body:JSON.stringify(body)});const d=await r.json();if(!r.ok)throw Error(d.error||'Stimmgenerierung fehlgeschlagen');return d;}
async function refresh(){try{const d=await api();if(!root.isConnected)return;const j=d.job;el('voice-status').textContent=(j?.phase||'Profil wählen und Text eingeben.')+(j?.gpu?' · '+j.gpu:'');el('voice-progress').hidden=j?.state!=='running';el('voice-start').disabled=busy||!ready||j?.state==='running';el('voice-cancel').disabled=j?.state!=='running';if(j?.state==='complete'&&j.id!==lastResult){lastResult=j.id;el('voice-result').innerHTML=`<h3>Ergebnis · ${Number(j.duration).toFixed(1)} Sekunden</h3><audio controls src="/api/v1/voice/audio?id=${e(j.id)}"></audio><p><a class="link" download="athena-stimme.wav" href="/api/v1/voice/audio?id=${e(j.id)}">WAV herunterladen</a></p>`;}}catch(err){if(root.isConnected)el('voice-error').textContent=err.message;}finally{if(root.isConnected)setTimeout(refresh,2000);}}
fetch('/api/v1/profiles').then(async r=>{if(!r.ok)throw Error('Profile konnten nicht geladen werden.');return r.json();}).then(d=>{if(!root.isConnected)return;const rows=d.profiles.filter(p=>p.runnable&&p.model?.repo==='openbmb/VoxCPM2');ready=!!rows.length;el('voice-profile').innerHTML=rows.map(p=>`<option value="${e(p.id)}">${e(p.name)}</option>`).join('')||'<option value="">VoxCPM2-Profil unter Profile → Einrichtung & Komponenten vervollständigen</option>';}).catch(err=>{if(root.isConnected)el('voice-error').textContent=err.message;});
el('voice-form').onsubmit=async event=>{event.preventDefault();busy=true;el('voice-start').disabled=true;el('voice-error').textContent='';el('voice-status').textContent='Auftrag wird gestartet …';try{const data=new FormData(event.target);if(!data.get('file')?.size)data.delete('file');await api('/start',data);}catch(err){el('voice-error').textContent=err.message;}finally{busy=false;}};
el('voice-cancel').onclick=async()=>{try{await api('/cancel',{});}catch(err){el('voice-error').textContent=err.message;}};refresh();
}
return {html,bind};
})();