feat: App TTS-Einstellungen vereinfacht + Mund-Button fuer lokales Muten

SettingsScreen:
- Piper-Reste entfernt (defaultVoice, highlightVoice, Speed-Slider,
  Highlight-Trigger-Info)
- Nur noch EIN Toggle 'Sprachausgabe auf diesem Geraet' — geraetelokal,
  persistent in aria_tts_enabled (AsyncStorage)
- Keine Config-Propagation mehr via RVS (das waere ja global gewesen)
- Hinweis dass Stimme + Voice-Cloning zentral in der Diagnose sind

ChatScreen: Mund-Button (👄 / 🤐)
- Neben Ohr-Button im Eingabebereich, NUR sichtbar wenn TTS im Setting
  grundsaetzlich aktiv ist
- Tap toggelt Mute: 👄 an / 🤐 rot gemutet
- Persistent in aria_tts_muted (AsyncStorage)
- Stoppt bei Muten sofort laufende Wiedergabe (stopPlayback)
- Settings-Toggle wird alle 2s gepollt damit Aenderungen greifen
  (einfache Loesung ohne globalen State-Context)

Audio-Handling respektiert lokalen Zustand
- Incoming audio/audio_pcm: nur abspielen wenn ttsDeviceEnabled && !ttsMuted
- Cache wird TROTZDEM immer geschrieben — Play-Button funktioniert
  spaeter aus Cache, auch waehrend Mute
- audioService.handlePcmChunk akzeptiert silent-Flag: skipt AudioTrack
  aber baut weiterhin den WAV-Cache pro messageId

Jedes Android-Geraet mit der App hat seinen eigenen Mute-Zustand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-19 22:33:36 +02:00
parent f801d99748
commit 40e48b046b
3 changed files with 78 additions and 154 deletions
+47 -6
View File
@@ -107,6 +107,9 @@ const ChatScreen: React.FC = () => {
const [searchVisible, setSearchVisible] = useState(false);
const [pendingAttachments, setPendingAttachments] = useState<{file: any, isPhoto: boolean}[]>([]);
const [agentActivity, setAgentActivity] = useState<{activity: string, tool: string}>({activity: 'idle', tool: ''});
// Gerätelokale TTS-Config: globaler Toggle (aus Settings) + temporäres Muten (Mund-Button)
const [ttsDeviceEnabled, setTtsDeviceEnabled] = useState(true);
const [ttsMuted, setTtsMuted] = useState(false);
const flatListRef = useRef<FlatList>(null);
const messageIdCounter = useRef(0);
@@ -117,6 +120,30 @@ const ChatScreen: React.FC = () => {
return `msg_${Date.now()}_${messageIdCounter.current}`;
};
// TTS-Settings beim Mount + bei Screen-Fokus neu laden (damit Settings-Toggle sofort greift)
useEffect(() => {
const loadTtsSettings = async () => {
const enabled = await AsyncStorage.getItem('aria_tts_enabled');
setTtsDeviceEnabled(enabled !== 'false'); // default true
const muted = await AsyncStorage.getItem('aria_tts_muted');
setTtsMuted(muted === 'true'); // default false
};
loadTtsSettings();
// Poll alle 2s um Settings-Aenderung mitzubekommen (einfache Loesung ohne Context)
const interval = setInterval(loadTtsSettings, 2000);
return () => clearInterval(interval);
}, []);
const toggleMute = useCallback(() => {
setTtsMuted(prev => {
const next = !prev;
AsyncStorage.setItem('aria_tts_muted', String(next));
// Bei Muten sofort laufende Wiedergabe stoppen
if (next) audioService.stopPlayback();
return next;
});
}, []);
// Chat-Verlauf aus AsyncStorage laden
const isInitialLoad = useRef(true);
useEffect(() => {
@@ -258,12 +285,13 @@ const ChatScreen: React.FC = () => {
});
}
// TTS-Audio abspielen wenn vorhanden
// TTS-Audio abspielen wenn vorhanden — respektiert geraetelokalen Mute/Disable
const canPlay = ttsDeviceEnabled && !ttsMuted;
if (message.type === 'audio' && message.payload.base64) {
const b64 = message.payload.base64 as string;
const refId = (message.payload.messageId as string) || '';
audioService.playAudio(b64);
// Wenn messageId mitgeliefert wurde: Audio in Cache speichern + Pfad in Message eintragen
if (canPlay) audioService.playAudio(b64);
// Cache IMMER schreiben — Play-Button soll auch bei Mute spaeter funktionieren
if (refId) {
audioService.cacheAudio(b64, refId).then(audioPath => {
if (!audioPath) return;
@@ -274,12 +302,11 @@ const ChatScreen: React.FC = () => {
}
}
// XTTS PCM-Stream: direkt an AudioTrack, bei final WAV-Cache schreiben
// XTTS PCM-Stream: Cache IMMER bauen, Playback nur wenn nicht gemutet
if (message.type === ('audio_pcm' as any)) {
const p = message.payload as any;
const p = { ...(message.payload as any), silent: !canPlay };
const refId = (p.messageId as string) || '';
audioService.handlePcmChunk(p).then((audioPath: any) => {
// Wenn final + Cache-Pfad zurueckkam, Message aktualisieren
if (p.final && audioPath && refId) {
setMessages(prev => prev.map(m =>
m.messageId === refId ? { ...m, audioPath } : m
@@ -825,6 +852,17 @@ const ChatScreen: React.FC = () => {
disabled={connectionState !== 'connected'}
wakeWordActive={wakeWordActive}
/>
{/* Mund-Button: TTS auf diesem Geraet muten/aufheben.
Nur sichtbar wenn TTS in den Settings grundsaetzlich aktiv ist. */}
{ttsDeviceEnabled && (
<TouchableOpacity
style={[styles.wakeWordBtn, ttsMuted && styles.mouthBtnMuted]}
onPress={toggleMute}
accessibilityLabel={ttsMuted ? 'Sprachausgabe einschalten' : 'Sprachausgabe stumm schalten'}
>
<Text style={styles.wakeWordIcon}>{ttsMuted ? '🤐' : '👄'}</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={[styles.wakeWordBtn, wakeWordActive && styles.wakeWordBtnActive]}
onPress={toggleWakeWord}
@@ -1042,6 +1080,9 @@ const styles = StyleSheet.create({
wakeWordBtnActive: {
backgroundColor: 'rgba(52, 199, 89, 0.3)',
},
mouthBtnMuted: {
backgroundColor: 'rgba(255, 59, 48, 0.25)',
},
wakeWordIcon: {
fontSize: 16,
},