Add profile presets and min-p controls, embeddings and VoxCPM workers

This commit is contained in:
Mikei386 committed 2026-10-02 18:42:29 +02:00
1 parent 74dd2ed8ba
commit a7b30b62d2
32 files changed
+615 -64

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
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};
})();