feat: NO_REPLY-Filter + Audio-Ducking + TTS-Cleanup

1) NO_REPLY Token wird in Bridge und Diagnostic erkannt und still
   verworfen. Toleranz fuer Variationen (Whitespace, Punkt, Quotes).
   Kein Chat-Eintrag, kein TTS.

2) AudioFocusModule (Kotlin) mit requestDuck / requestExclusive /
   release. AudioService ruft:
   - requestExclusive() bei Aufnahme-Start → andere Apps pausieren
   - requestDuck() bei TTS-Playback-Start → andere Apps leiser
   - release() bei Stop/Queue-Ende
   MainApplication registriert AudioFocusPackage.

3) clean_text_for_tts() in Bridge — zentrale Aufbereitung:
   - <voice>...</voice> Tag wird bevorzugt (falls ARIA es schreibt)
   - Code-Bloecke (``` und `) komplett raus
   - Markdown (Fett/Kursiv/Links/Headings/Listen) geschleift
   - Einheiten ausgeschrieben: 22GB → 22 Gigabyte, 85% → 85 Prozent
   - Abkuerzungen buchstabiert: CPU → C P U, API → A P I
   - URLs durch "ein Link" ersetzt
   Genutzt in VoiceEngine.synthesize und im XTTS-Request — Chat-Text
   an die App bleibt unveraendert (original Markdown), nur TTS kriegt
   die aufbereitete Version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 16:10:54 +02:00
parent 23add7a107
commit 8b0a72dc9b
6 changed files with 255 additions and 18 deletions
+24 -1
View File
@@ -6,7 +6,7 @@
* Nutzt react-native-audio-recorder-player fuer Aufnahme.
*/
import { Platform, PermissionsAndroid } from 'react-native';
import { Platform, PermissionsAndroid, NativeModules } from 'react-native';
import Sound from 'react-native-sound';
import RNFS from 'react-native-fs';
import AudioRecorderPlayer, {
@@ -16,6 +16,15 @@ import AudioRecorderPlayer, {
OutputFormatAndroidType,
} from 'react-native-audio-recorder-player';
// Native Module fuer Audio-Focus (Ducking/Muten anderer Apps)
const { AudioFocus } = NativeModules as {
AudioFocus?: {
requestDuck: () => Promise<boolean>;
requestExclusive: () => Promise<boolean>;
release: () => Promise<boolean>;
};
};
// --- Typen ---
export interface RecordingResult {
@@ -172,6 +181,9 @@ class AudioService {
this.speechStartTime = 0;
this.setState('recording');
// Andere Apps waehrend der Aufnahme pausieren (Musik, Videos etc.)
AudioFocus?.requestExclusive().catch(() => {});
// VAD aktivieren
this.vadEnabled = autoStop;
if (autoStop) {
@@ -220,6 +232,9 @@ class AudioService {
await this.recorder.stopRecorder();
this.recorder.removeRecordBackListener();
// Audio-Focus freigeben — andere Apps duerfen wieder
AudioFocus?.release().catch(() => {});
const durationMs = Date.now() - this.recordingStartTime;
const hadSpeech = this.speechDetected;
@@ -278,11 +293,17 @@ class AudioService {
private async _playNext(): Promise<void> {
if (this.audioQueue.length === 0) {
this.isPlaying = false;
// Audio-Focus abgeben → andere Apps volle Lautstaerke
AudioFocus?.release().catch(() => {});
// Alle Audio-Teile abgespielt → Listener benachrichtigen
this.playbackFinishedListeners.forEach(cb => cb());
return;
}
// Beim ersten Playback-Start: andere Apps ducken
if (!this.isPlaying) {
AudioFocus?.requestDuck().catch(() => {});
}
this.isPlaying = true;
// Preloaded Sound verwenden wenn verfuegbar, sonst neu laden
@@ -358,6 +379,8 @@ class AudioService {
if (this.preloadedPath) RNFS.unlink(this.preloadedPath).catch(() => {});
this.preloadedPath = '';
}
// Audio-Focus freigeben
AudioFocus?.release().catch(() => {});
}
// --- Status & Callbacks ---