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:
@@ -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,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user