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
+18
View File
@@ -92,6 +92,24 @@ export const CONV_WINDOW_MIN_SEC = 3.0;
export const CONV_WINDOW_MAX_SEC = 20.0;
export const CONV_WINDOW_STORAGE_KEY = 'aria_conv_window_sec';
// TTS-Wiedergabegeschwindigkeit — wird pro Geraet gespeichert und an die
// Bridge mitgegeben (speed-Param im F5-TTS infer()). 1.0 = normal.
export const TTS_SPEED_DEFAULT = 1.0;
export const TTS_SPEED_MIN = 0.5;
export const TTS_SPEED_MAX = 2.0;
export const TTS_SPEED_STORAGE_KEY = 'aria_tts_speed';
export async function loadTtsSpeed(): Promise<number> {
try {
const raw = await AsyncStorage.getItem(TTS_SPEED_STORAGE_KEY);
if (raw != null) {
const n = parseFloat(raw);
if (isFinite(n) && n >= TTS_SPEED_MIN && n <= TTS_SPEED_MAX) return n;
}
} catch {}
return TTS_SPEED_DEFAULT;
}
export async function loadConvWindowMs(): Promise<number> {
try {
const raw = await AsyncStorage.getItem(CONV_WINDOW_STORAGE_KEY);
+24 -4
View File
@@ -90,12 +90,32 @@ class WakeWordService {
if (this.initInProgress) return this.initInProgress;
this.initInProgress = (async () => {
try {
const { PorcupineManager } = require('@picovoice/porcupine-react-native');
// Built-In Keyword-Identifier sind lower-case strings im SDK
const porcupineRN = require('@picovoice/porcupine-react-native');
const { PorcupineManager, BuiltInKeywords } = porcupineRN;
// Manche Porcupine-Versionen wollen das BuiltInKeywords-Enum (Objekt
// mit keys wie JARVIS, COMPUTER, HEY_GOOGLE), andere akzeptieren
// den String direkt. Mappen mit Fallback auf String:
const enumKey = this.keyword.toUpperCase().replace(/\s+/g, '_');
const kw = (BuiltInKeywords && BuiltInKeywords[enumKey]) || this.keyword;
console.log('[WakeWord] Porcupine init: keyword=%s (resolved=%s)',
this.keyword, typeof kw === 'string' ? kw : '[enum]');
this.porcupine = await PorcupineManager.fromBuiltInKeywords(
this.accessKey,
[this.keyword],
(_keywordIndex: number) => this.onWakeDetected(),
[kw],
(keywordIndex: number) => {
console.log('[WakeWord] Porcupine callback fired (index=%d)', keywordIndex);
this.onWakeDetected().catch(err =>
console.warn('[WakeWord] onWakeDetected crashed:', err));
},
// Error handler (wenn Porcupine im Background-Thread crashed,
// z.B. beim Audio-Engine-Konflikt mit audio-recorder-player)
(error: any) => {
console.warn('[WakeWord] Porcupine runtime error:', error?.message || error);
// Nicht in Loop crashen — state zurueck auf off damit der User
// mit dem Aufnahme-Button wieder normal arbeiten kann
this.setState('off');
this.disposePorcupine().catch(() => {});
},
);
console.log('[WakeWord] Porcupine init OK (keyword=%s)', this.keyword);
return true;