feat: Bug-Runde + 5 App/Diagnostic-Features

Bugs:
- App Mute-/Auto-Playback: onMessage-Closure hielt stale ttsDeviceEnabled/
  ttsMuted → Mute wurde ignoriert + AsyncStorage-Load kam nicht durch.
  Fix via ttsCanPlayRef (live gespiegelt) statt Closure-Variablen.
- App Zombie-Recording: toggleWakeWord hat die laufende Aufnahme nicht
  gestoppt → audioService.recordingState blieb 'recording' → normaler
  Aufnahme-Button wirkungslos. Fix: await stopRecording() vor stop().
- Porcupine robuster: BuiltInKeywords-Enum Mapping mit String-Fallback,
  errorCallback fuer Runtime-Crashes (state zurueck auf off statt
  App-Crash), mehr Logging damit man beim naechsten Issue debuggen kann.

App-Features:
- MessageText Komponente: Text ist durchgehend selektierbar, erkennt
  URLs (http/https), E-Mails, Telefonnummern und macht sie anklickbar
  (oeffnet Browser / Mail-App / Android-Dialer via Linking).
- TTS-Wiedergabegeschwindigkeit pro Geraet einstellbar (Settings ->
  "Sprechgeschwindigkeit", 0.5-2.0 in 0.1-Schritten, Default 1.0).
  Wird als speed-Param an die F5-TTS-Bridge durchgereicht.

Bridge-Durchreichen:
- ChatScreen: speed aus AsyncStorage via ttsSpeedRef, an chat/audio/
  tts_request mitgeschickt
- aria-bridge: _next_speed_override wie voice_override, an xtts_request
  weitergereicht
- f5tts-bridge: speed-Param an F5TTS.infer() durchgereicht

Diagnostic-Feature:
- Voice-Preview-Button (Play-Icon) vor dem Delete-X in der Stimmen-Liste
- Modal mit Textfeld (Default-Beispieltext wird bei jedem Oeffnen neu
  gesetzt) und Play-Button
- Server sammelt audio_pcm Frames der Preview-Anfrage, baut WAV,
  schickt base64 zurueck, Browser spielt im <audio>-Tag ab
- 60s Timeout-Safety-Net

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:24:02 +02:00
parent 2264f4e3bc
commit 190352820c
10 changed files with 439 additions and 20 deletions
+31 -1
View File
@@ -541,6 +541,9 @@ class ARIABridge:
# Wird fuer die direkt folgende ARIA-Antwort genutzt und dann zurueckgesetzt.
# So kann jedes Geraet seine bevorzugte Stimme bekommen (pro Request).
self._next_voice_override: Optional[str] = None
# Gleiche Logik fuer die Wiedergabegeschwindigkeit (F5-TTS speed-Param,
# App-Setting aria_tts_speed, 1.0 = normal).
self._next_speed_override: Optional[float] = None
# STT-Requests die aktuell auf Antwort von der whisper-bridge (Gamebox) warten.
# requestId → Future mit dem Text (oder None bei Fehler).
self._pending_stt: dict[str, asyncio.Future] = {}
@@ -911,6 +914,12 @@ class ARIABridge:
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
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
if not tts_text:
logger.info("[core] TTS-Text leer nach Cleanup — uebersprungen")
@@ -926,6 +935,7 @@ class ARIABridge:
"payload": {
"text": tts_text,
"voice": xtts_voice,
"speed": xtts_speed,
"language": "de",
"requestId": xtts_request_id,
"messageId": message_id,
@@ -1163,6 +1173,13 @@ class ARIABridge:
if voice_override:
self._next_voice_override = voice_override
logger.info("[rvs] Voice-Override fuer naechste Antwort: %s", voice_override)
# Speed-Override (TTS-Wiedergabegeschwindigkeit, pro Geraet)
try:
speed = float(payload.get("speed", 0) or 0)
if 0.25 <= speed <= 4.0:
self._next_speed_override = speed
except (TypeError, ValueError):
pass
if text:
logger.info("[rvs] App-Chat: '%s'", text[:80])
await self.send_to_core(text, source="app")
@@ -1215,8 +1232,14 @@ class ARIABridge:
if not text:
return
tts_text = clean_text_for_tts(text) or text
# Voice aus App-Payload gewinnt, sonst global
# Voice + Speed aus App-Payload gewinnen, sonst global/default
xtts_voice = payload.get("voice", "") or getattr(self, 'xtts_voice', '')
try:
xtts_speed = float(payload.get("speed", 0) or 0)
if not (0.25 <= xtts_speed <= 4.0):
xtts_speed = 1.0
except (TypeError, ValueError):
xtts_speed = 1.0
try:
xtts_request_id = str(uuid.uuid4())
if message_id:
@@ -1226,6 +1249,7 @@ class ARIABridge:
"payload": {
"text": tts_text,
"voice": xtts_voice,
"speed": xtts_speed,
"language": "de",
"requestId": xtts_request_id,
"messageId": message_id,
@@ -1424,6 +1448,12 @@ class ARIABridge:
if voice_override:
self._next_voice_override = voice_override
logger.info("[rvs] Voice-Override (via Audio): %s", voice_override)
try:
speed = float(payload.get("speed", 0) or 0)
if 0.25 <= speed <= 4.0:
self._next_speed_override = speed
except (TypeError, ValueError):
pass
logger.info("[rvs] Audio empfangen: %s, %dms, %dKB",
mime_type, duration_ms, len(audio_b64) // 1365)
asyncio.create_task(self._process_app_audio(audio_b64, mime_type))