fix(voice): Stimmen-Wechsel greift wieder — Override bleibt bis naechster Chat-Event

Bug: Voice-Override wurde nach der ersten ARIA-Antwort konsumiert.
Eine ARIA-Antwort triggert aber oft mehrere TTS-Calls (Tool-Use →
Zwischenmeldung → finale Antwort). Der erste nutzte die neue Stimme,
alle folgenden fielen auf self.xtts_voice (= alte Voice aus
voice_config.json) zurueck. Die App schickt nie ein config-Update,
daher blieb voice_config.json fuer immer auf der alten Stimme.

Neue Semantik:
- chat-/audio-Event mit voice="X" → Override="X", gilt fuer alle
  folgenden TTS-Calls bis zum naechsten chat-Event
- chat-Event mit voice="" → Override geloescht, fallback auf
  Default-Voice (voice_config.json / Diagnostic)
- chat-Event ohne voice-Field → Override unveraendert

Audio-Send in ChatScreen.tsx (Push-to-Talk-Pfad) gab voice/speed
gar nicht mit; jetzt konsistent mit dem Tap-to-Talk-Pfad.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-26 20:04:19 +02:00
parent b588dd7e3b
commit abc5b971f4
2 changed files with 34 additions and 32 deletions

View File

@ -619,6 +619,8 @@ const ChatScreen: React.FC = () => {
base64: result.base64, base64: result.base64,
durationMs: result.durationMs, durationMs: result.durationMs,
mimeType: result.mimeType, mimeType: result.mimeType,
voice: localXttsVoiceRef.current,
speed: ttsSpeedRef.current,
...(location && { location }), ...(location && { location }),
}); });
}, [getCurrentLocation]); }, [getCurrentLocation]);

View File

@ -907,18 +907,13 @@ class ARIABridge:
logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name) logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name)
return return
# Voice bestimmen: App-Override fuer diesen Request > globale Default-Voice # Voice bestimmen: App-Override (gesetzt durch letzten chat-Event) > globale
# Default-Voice. Der Override wird NICHT pro Antwort verbraucht — sonst nutzt
# eine Multi-Turn-Antwort von ARIA (Tool-Use + finale Antwort) ab dem zweiten
# TTS-Call wieder die alte Default-Stimme. Der Override bleibt gueltig bis
# zum naechsten chat-Event, wo er entweder ueberschrieben oder geloescht wird.
xtts_voice = self._next_voice_override or getattr(self, 'xtts_voice', '') xtts_voice = self._next_voice_override or getattr(self, 'xtts_voice', '')
# Override verbrauchen (gilt nur fuer genau diese naechste Antwort)
if self._next_voice_override:
logger.info("[core] Nutze Voice-Override: %s", self._next_voice_override)
self._next_voice_override = None
# Speed ebenfalls aus App-Override nehmen (fallback 1.0)
xtts_speed = self._next_speed_override or 1.0 xtts_speed = self._next_speed_override or 1.0
if self._next_speed_override:
logger.info("[core] Nutze Speed-Override: %.2fx", self._next_speed_override)
self._next_speed_override = None
tts_text = tts_text_preview or text tts_text = tts_text_preview or text
if not tts_text: if not tts_text:
@ -1169,18 +1164,22 @@ class ARIABridge:
if sender in ("aria", "stt"): if sender in ("aria", "stt"):
return return
text = payload.get("text", "") text = payload.get("text", "")
# Voice-Override fuer die naechste ARIA-Antwort merken # Voice-Override fuer Folgenachrichten setzen — gilt bis zum naechsten
voice_override = payload.get("voice", "") # chat-Event. Leerer String "" = explizit Default-Voice (override loeschen).
if voice_override: # Field nicht gesendet = vorherigen Override unveraendert lassen (z.B. wenn
self._next_voice_override = voice_override # cancel_request oder anderer Service die App umgeht).
logger.info("[rvs] Voice-Override fuer naechste Antwort: %s", voice_override) if "voice" in payload:
voice_override = payload.get("voice", "") or ""
self._next_voice_override = voice_override or None
logger.info("[rvs] Voice fuer Antworten: %s",
self._next_voice_override or "(Default)")
# Speed-Override (TTS-Wiedergabegeschwindigkeit, pro Geraet) # Speed-Override (TTS-Wiedergabegeschwindigkeit, pro Geraet)
try: if "speed" in payload:
speed = float(payload.get("speed", 0) or 0) try:
if 0.1 <= speed <= 5.0: speed = float(payload.get("speed", 0) or 0)
self._next_speed_override = speed self._next_speed_override = speed if 0.1 <= speed <= 5.0 else None
except (TypeError, ValueError): except (TypeError, ValueError):
pass self._next_speed_override = None
if text: if text:
logger.info("[rvs] App-Chat: '%s'", text[:80]) logger.info("[rvs] App-Chat: '%s'", text[:80])
await self.send_to_core(text, source="app") await self.send_to_core(text, source="app")
@ -1444,17 +1443,18 @@ class ARIABridge:
if not audio_b64: if not audio_b64:
logger.warning("[rvs] Audio ohne Daten empfangen") logger.warning("[rvs] Audio ohne Daten empfangen")
return return
# Voice-Override fuer die kommende ARIA-Antwort (App-lokal gewaehlt) # Voice-Override fuer Folgenachrichten — gleiche Semantik wie beim chat-Event.
voice_override = payload.get("voice", "") if "voice" in payload:
if voice_override: voice_override = payload.get("voice", "") or ""
self._next_voice_override = voice_override self._next_voice_override = voice_override or None
logger.info("[rvs] Voice-Override (via Audio): %s", voice_override) logger.info("[rvs] Voice fuer Antworten (via Audio): %s",
try: self._next_voice_override or "(Default)")
speed = float(payload.get("speed", 0) or 0) if "speed" in payload:
if 0.1 <= speed <= 5.0: try:
self._next_speed_override = speed speed = float(payload.get("speed", 0) or 0)
except (TypeError, ValueError): self._next_speed_override = speed if 0.1 <= speed <= 5.0 else None
pass except (TypeError, ValueError):
self._next_speed_override = None
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB", logger.info("[rvs] Audio empfangen: %s, %dms, %dKB",
mime_type, duration_ms, len(audio_b64) // 1365) mime_type, duration_ms, len(audio_b64) // 1365)
asyncio.create_task(self._process_app_audio(audio_b64, mime_type)) asyncio.create_task(self._process_app_audio(audio_b64, mime_type))