diff --git a/android/src/screens/SettingsScreen.tsx b/android/src/screens/SettingsScreen.tsx index 2388ffb..018876c 100644 --- a/android/src/screens/SettingsScreen.tsx +++ b/android/src/screens/SettingsScreen.tsx @@ -482,7 +482,7 @@ const SettingsScreen: React.FC = () => { { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); }} + onPress={() => { setDefaultVoice('ramona'); AsyncStorage.setItem('aria_default_voice', 'ramona'); rvs.send('config' as any, { defaultVoice: 'ramona' }); }} > {'\uD83D\uDE4E\u200D\u2640\uFE0F'} Ramona @@ -490,7 +490,7 @@ const SettingsScreen: React.FC = () => { { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); }} + onPress={() => { setDefaultVoice('thorsten'); AsyncStorage.setItem('aria_default_voice', 'thorsten'); rvs.send('config' as any, { defaultVoice: 'thorsten' }); }} > {'\uD83E\uDDD4'} Thorsten @@ -506,14 +506,14 @@ const SettingsScreen: React.FC = () => { { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); }} + onPress={() => { setHighlightVoice('thorsten'); AsyncStorage.setItem('aria_highlight_voice', 'thorsten'); rvs.send('config' as any, { highlightVoice: 'thorsten' }); }} > {'\uD83E\uDDD4'} Thorsten { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); }} + onPress={() => { setHighlightVoice('ramona'); AsyncStorage.setItem('aria_highlight_voice', 'ramona'); rvs.send('config' as any, { highlightVoice: 'ramona' }); }} > {'\uD83D\uDE4E\u200D\u2640\uFE0F'} Ramona diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index 1c19a14..98ea8ad 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -129,6 +129,8 @@ class VoiceEngine: def __init__(self, voices_dir: Path) -> None: self.voices_dir = voices_dir self.voices: dict[str, PiperVoice] = {} + self.default_voice = "ramona" + self.highlight_voice = "thorsten" def initialize(self) -> None: """Laedt die Piper-Stimmen aus dem Voices-Verzeichnis.""" @@ -172,14 +174,14 @@ class VoiceEngine: if requested_voice and requested_voice in self.voices: return requested_voice - # Epische Trigger pruefen + # Highlight-Trigger pruefen text_lower = text.lower() for trigger in EPIC_TRIGGERS: if trigger in text_lower: - logger.info("Epischer Trigger erkannt: '%s' — Thorsten spricht", trigger) - return "thorsten" + logger.info("Highlight-Trigger erkannt: '%s' — %s spricht", trigger, self.highlight_voice) + return self.highlight_voice - return "ramona" + return self.default_voice def synthesize(self, text: str, voice_name: str = "ramona") -> Optional[bytes]: """Erzeugt Audio-Daten aus Text mit der gewaehlten Stimme. @@ -791,7 +793,7 @@ class ARIABridge: }) # TTS-Audio rendern und an die App senden (wenn Modus es erlaubt) - if should_speak(self.current_mode, is_critical): + if getattr(self, 'tts_enabled', True) and should_speak(self.current_mode, is_critical): audio_data = self.voice_engine.synthesize(text, voice_name) if audio_data: audio_b64 = base64.b64encode(audio_data).decode("ascii") @@ -947,6 +949,23 @@ class ARIABridge: sender = payload.get("sender", "") if sender in ("aria", "stt"): return + + elif msg_type == "config": + # Konfiguration von App/Diagnostic empfangen + if "defaultVoice" in payload: + new_voice = payload["defaultVoice"] + if new_voice in self.voice_engine.voices: + self.voice_engine.default_voice = new_voice + logger.info("[rvs] Standard-Stimme gewechselt: %s", new_voice) + if "highlightVoice" in payload: + new_voice = payload["highlightVoice"] + if new_voice in self.voice_engine.voices: + self.voice_engine.highlight_voice = new_voice + logger.info("[rvs] Highlight-Stimme gewechselt: %s", new_voice) + if "ttsEnabled" in payload: + self.tts_enabled = bool(payload["ttsEnabled"]) + logger.info("[rvs] TTS %s", "aktiviert" if self.tts_enabled else "deaktiviert") + return text = payload.get("text", "") if text: logger.info("[rvs] App-Chat: '%s'", text[:80]) diff --git a/diagnostic/index.html b/diagnostic/index.html index 832b5c4..44ff380 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -396,6 +396,31 @@ + +
+

Sprachausgabe

+
+
+ + +
+
+ + +
+
+ + +
+
+
+

Highlight-Trigger

@@ -1106,6 +1131,14 @@ }, 120000); } + // ── Stimmen-Config ────────────────────────── + function sendVoiceConfig() { + const defaultVoice = document.getElementById('diag-default-voice').value; + const highlightVoice = document.getElementById('diag-highlight-voice').value; + const ttsEnabled = document.getElementById('diag-tts-enabled').checked; + send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled }); + } + // ── Highlight-Trigger ──────────────────────── function loadHighlightTriggers() { send({ action: 'get_triggers' }); diff --git a/diagnostic/server.js b/diagnostic/server.js index 1441d2d..a4357ad 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -549,6 +549,18 @@ function connectRVS(forcePlain) { }); } +function sendToRVS_raw(msgObj) { + if (!RVS_HOST || !RVS_TOKEN) return; + const proto = RVS_TLS === "true" ? "wss" : "ws"; + const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`; + const freshWs = new WebSocket(url); + freshWs.on("open", () => { + freshWs.send(JSON.stringify(msgObj)); + setTimeout(() => { try { freshWs.close(); } catch (_) {} }, 5000); + }); + freshWs.on("error", () => {}); +} + function sendToRVS(text, isPipeline) { if (!RVS_HOST || !RVS_TOKEN) { log("error", "rvs", "Nicht konfiguriert"); @@ -1147,6 +1159,14 @@ wss.on("connection", (ws) => { if (ws._sshSock) ws._sshSock.write(msg.data); } else if (msg.action === "live_ssh_close") { if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; } + } else if (msg.action === "send_voice_config") { + // Stimmen-Config an Bridge via RVS senden + sendToRVS_raw({ type: "config", payload: { + defaultVoice: msg.defaultVoice, + highlightVoice: msg.highlightVoice, + ttsEnabled: msg.ttsEnabled, + }, timestamp: Date.now() }); + log("info", "server", `Voice-Config gesendet: default=${msg.defaultVoice}, highlight=${msg.highlightVoice}, tts=${msg.ttsEnabled}`); } else if (msg.action === "get_triggers") { handleGetTriggers(ws); } else if (msg.action === "save_triggers") { diff --git a/issue.md b/issue.md index 801449a..5b1a672 100644 --- a/issue.md +++ b/issue.md @@ -5,8 +5,9 @@ sprachnachrichten werden nicht als zweite nachricht dargestellt, damit man weiß # ende -cache leeren, bilder werden nicht neu geladen beim antippen. +# erledigt cache leeren, bilder werden nicht neu geladen beim antippen. autoload geht nicht +# ende wenn man auf das ohr zum hören klickt stürzt ab diff --git a/rvs/server.js b/rvs/server.js index a303a59..a1e7c84 100644 --- a/rvs/server.js +++ b/rvs/server.js @@ -9,7 +9,7 @@ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10); // Erlaubte Nachrichtentypen — alles andere wird verworfen const ALLOWED_TYPES = new Set([ "chat", "audio", "file", "location", "mode", "log", "event", "heartbeat", - "file_request", "file_response", "file_saved", "stt_result", + "file_request", "file_response", "file_saved", "stt_result", "config", ]); // Token-Raum: token -> { clients: Set }