fix/feat: XTTS-Voice korrekt persistiert, Loeschen + Voice-per-Request

Bug-Fix: Voice-Auswahl verschwand nach Page-Load
- xtts_voices_list Handler rebuildet das Dropdown — vorheriger select.value
  ging dabei verloren. Jetzt wird der Wert gemerkt und nach Rebuild
  wiederhergestellt (falls die Stimme noch existiert).

Feature: Stimmen loeschen (Diagnostic)
- XTTS-Bridge: neuer handleDeleteVoice — entfernt /voices/<name>.wav
  und schickt aktualisierte Liste per xtts_voices_list
- RVS: xtts_delete_voice in ALLOWED_TYPES
- Diagnostic Server: Action xtts_delete_voice forwarded via RVS
- Diagnostic UI: renderVoiceList zeigt alle Custom-Voices mit X-Button
  Bei Loeschen der gerade aktiven Stimme: auf Default zuruecksetzen

Feature: Voice-per-Request in Bridge
- App kann mit jedem Chat ein voice-Feld mitschicken
- Bridge merkt sich _next_voice_override, nutzt es fuer die NAECHSTE
  ARIA-Antwort (einmalig, dann reset)
- tts_request (Play-Button) akzeptiert voice im Payload als Override
- Fallback: globale xtts_voice aus voice_config.json
- So kann jedes Geraet seine eigene Stimme haben ohne den globalen
  Default zu aendern

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:43:26 +02:00
parent 40e48b046b
commit fc2438be2d
5 changed files with 91 additions and 6 deletions
+42 -3
View File
@@ -419,6 +419,9 @@
<button class="btn secondary" onclick="loadXTTSVoices()" style="padding:4px 10px;font-size:11px;">Laden</button>
</div>
<!-- Gecloned Stimmen — Liste mit Loeschen -->
<div id="xtts-voice-list" style="margin-bottom:12px;"></div>
<!-- Voice Cloning -->
<div style="background:#1E1E2E;border-radius:8px;padding:12px;margin-top:8px;">
<div style="color:#0096FF;font-size:13px;font-weight:600;margin-bottom:8px;">Stimme klonen</div>
@@ -752,16 +755,23 @@
if (msg.type === 'xtts_voices_list') {
const select = document.getElementById('diag-xtts-voice');
// Behalte erste Option (Default)
// Aktuelle Auswahl merken damit Rebuild sie nicht zerstoert
const previouslySelected = select.value;
while (select.options.length > 1) select.remove(1);
for (const v of (msg.payload?.voices || [])) {
const voices = msg.payload?.voices || [];
for (const v of voices) {
const opt = document.createElement('option');
opt.value = v.name;
opt.textContent = `${v.name} (${(v.size / 1024).toFixed(0)}KB)`;
select.appendChild(opt);
}
document.getElementById('xtts-status').textContent = `XTTS: ${msg.payload?.voices?.length || 0} Stimme(n) verfuegbar`;
// Wenn die vorherige Auswahl weiter existiert → wiederherstellen
if (previouslySelected && voices.some(v => v.name === previouslySelected)) {
select.value = previouslySelected;
}
document.getElementById('xtts-status').textContent = `XTTS: ${voices.length} Stimme(n) verfuegbar`;
document.getElementById('xtts-status').style.color = '#34C759';
renderVoiceList(voices);
return;
}
if (msg.type === 'xtts_voice_saved') {
@@ -1356,6 +1366,35 @@
}
// ── XTTS Panel ─────────────────────────────
function renderVoiceList(voices) {
const box = document.getElementById('xtts-voice-list');
if (!box) return;
if (!voices || voices.length === 0) {
box.innerHTML = '<div style="color:#555570;font-size:11px;">Noch keine eigenen Stimmen vorhanden.</div>';
return;
}
let html = '<div style="color:#8888AA;font-size:11px;margin-bottom:4px;">Geclonte Stimmen:</div>';
html += '<div style="display:flex;flex-direction:column;gap:4px;">';
for (const v of voices) {
const esc = (s) => String(s).replace(/[&<>"']/g, c => ({ "&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;" }[c]));
html += `<div style="display:flex;align-items:center;gap:8px;background:#1E1E2E;border-radius:4px;padding:4px 8px;font-size:12px;">`
+ `<span style="flex:1;color:#E0E0F0;">${esc(v.name)}</span>`
+ `<span style="color:#555570;font-size:10px;">${(v.size/1024).toFixed(0)}KB</span>`
+ `<button class="btn secondary" onclick="deleteXttsVoice('${esc(v.name).replace(/'/g, "\\'")}')" style="padding:2px 8px;font-size:10px;color:#FF6B6B;" title="Stimme loeschen">X</button>`
+ `</div>`;
}
html += '</div>';
box.innerHTML = html;
}
function deleteXttsVoice(name) {
if (!confirm(`Stimme "${name}" endgueltig loeschen?`)) return;
send({ action: 'xtts_delete_voice', name });
// Bei aktueller Auswahl: auf Default zuruecksetzen
const sel = document.getElementById('diag-xtts-voice');
if (sel.value === name) { sel.value = ''; sendVoiceConfig(); }
}
// Legacy no-op (XTTS ist jetzt die einzige Engine, kein Panel-Toggle noetig)
function toggleXTTSPanel() {
void 0;
+4
View File
@@ -1339,6 +1339,10 @@ wss.on("connection", (ws) => {
} else if (msg.action === "xtts_list_voices") {
// Frische Verbindung die auf Antwort wartet
sendToRVS_withResponse("xtts_list_voices", {}, "xtts_voices_list", ws);
} else if (msg.action === "xtts_delete_voice") {
// Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste
sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() });
log("info", "server", `Voice-Delete '${msg.name}' an XTTS-Bridge gesendet`);
} else if (msg.action === "get_voice_config") {
handleGetVoiceConfig(ws);
} else if (msg.action === "send_voice_config") {