From 095385a4a5b2e46d5bd5c838690d9c7b30511a2a Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 27 Dec 2025 02:20:41 +0100 Subject: [PATCH] mute funtion und warten bis nachricht gesendet --- README.md | 9 ++ docs/setup_guide.md | 10 ++ python_bridge/chat_audio_bridge.py | 150 ++++++++++++++++++++++++----- 3 files changed, 146 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index a60f4f9..e17ecd7 100644 --- a/README.md +++ b/README.md @@ -167,10 +167,19 @@ Claude verwendet diese Befehle in eckigen Klammern: - **Paralelle Konversation** - Erkunden UND quatschen gleichzeitig - **Sprachausgabe** - Claude redet mit dir (TTS) - **Spracheingabe** - Du redest mit Claude (STT) +- **Mute/Unmute** - Mikrofon per Tastendruck stummschalten - **Hinderniserkennung** - Ultraschall & IMU - **Touch-Display** - Notfall-Stopp & Status - **Termux Support** - Läuft auch auf Android! +## Keyboard-Shortcuts (Bridge) + +| Taste | Funktion | +|-------|----------| +| **M** | Mikrofon Mute/Unmute | +| **Q** | Bridge beenden | +| **Ctrl+C** | Bridge beenden | + --- ## Sicherheit diff --git a/docs/setup_guide.md b/docs/setup_guide.md index a27af63..8375ca4 100644 --- a/docs/setup_guide.md +++ b/docs/setup_guide.md @@ -229,6 +229,16 @@ python chat_audio_bridge.py -d python chat_audio_bridge.py -c config.local.yaml ``` +### 2.6 Keyboard-Shortcuts während der Bridge läuft + +| Taste | Funktion | +|-------|----------| +| **M** | Mikrofon Mute/Unmute - schaltet STT stumm | +| **Q** | Bridge beenden | +| **Ctrl+C** | Bridge beenden | + +**Tipp:** Nutze Mute wenn du nicht willst dass Hintergrundgeräusche oder Gespräche mit anderen aufgenommen werden. Das Mikrofon nimmt sonst alles auf! + --- ## Teil 3: Hardware zusammenbauen diff --git a/python_bridge/chat_audio_bridge.py b/python_bridge/chat_audio_bridge.py index 4d25f97..ada729e 100755 --- a/python_bridge/chat_audio_bridge.py +++ b/python_bridge/chat_audio_bridge.py @@ -114,6 +114,14 @@ class ClaudesEyesAudioBridge: self._stefan_buffer: list = [] self._stefan_buffer_lock = threading.Lock() + # Send-Lock: Verhindert dass TICKs während Senden reinkommen + self._sending = threading.Event() + self._sending.set() # Anfangs nicht am Senden (set = frei) + + # Mute-Flag: Wenn True, ignoriert STT alle Eingaben + self._muted = False + self._mute_lock = threading.Lock() + def _load_config(self, config_path: str) -> dict: """Lädt die Konfiguration""" path = Path(config_path) @@ -252,8 +260,13 @@ class ClaudesEyesAudioBridge: t3.start() threads.append(t3) + # Thread 4: Keyboard-Listener für Mute-Toggle + t4 = threading.Thread(target=self._keyboard_loop, name="Keyboard", daemon=True) + t4.start() + threads.append(t4) + console.print("[cyan]Bridge läuft![/cyan]") - console.print("[dim]Drücke Ctrl+C zum Beenden[/dim]\n") + console.print("[dim]Drücke 'M' für Mute/Unmute, Ctrl+C zum Beenden[/dim]\n") # Sende Startsignal an Claude und warte auf [READY] if not self._send_start_signal(): @@ -435,33 +448,43 @@ Erst dann starten die automatischen TICKs mit Bildern!""" # Stefan-Buffer holen (falls er was gesagt hat) stefan_text = self._get_and_clear_stefan_buffer() + # Warte bis vorheriges Senden fertig ist + self._sending.wait() + # Nächsten TICK senden (mit oder ohne Bild) with self._lock: - # Erst Bild hochladen wenn aktiviert - if upload_images: - # Bild holen und hochladen - if not self.chat.fetch_image_from_esp32(): - logger.warning("Konnte kein Bild vom ESP32 holen") - elif not self.chat.upload_image_to_chat(): - logger.warning("Konnte Bild nicht hochladen") + # Signalisiere dass wir senden + self._sending.clear() - # Nachricht zusammenbauen - if stefan_text: - # Stefan hat was gesagt → Mit TICK senden - tick_message = f"[TICK]\n\nStefan sagt: {stefan_text}" - console.print(f"[cyan]→ TICK mit Stefan-Buffer: \"{stefan_text[:50]}...\"[/cyan]" if len(stefan_text) > 50 else f"[cyan]→ TICK mit Stefan-Buffer: \"{stefan_text}\"[/cyan]") - else: - # Nur TICK - tick_message = "[TICK]" + try: + # Erst Bild hochladen wenn aktiviert + if upload_images: + # Bild holen und hochladen + if not self.chat.fetch_image_from_esp32(): + logger.warning("Konnte kein Bild vom ESP32 holen") + elif not self.chat.upload_image_to_chat(): + logger.warning("Konnte Bild nicht hochladen") - success = self.chat.send_message(tick_message) + # Nachricht zusammenbauen + if stefan_text: + # Stefan hat was gesagt → Mit TICK senden + tick_message = f"[TICK]\n\nStefan sagt: {stefan_text}" + console.print(f"[cyan]→ TICK mit Stefan-Buffer: \"{stefan_text[:50]}...\"[/cyan]" if len(stefan_text) > 50 else f"[cyan]→ TICK mit Stefan-Buffer: \"{stefan_text}\"[/cyan]") + else: + # Nur TICK + tick_message = "[TICK]" - if success: - self.stats.ticks_sent += 1 - self.stats.consecutive_errors = 0 # Reset - logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if upload_images else "") + (f" + Stefan: {stefan_text[:30]}" if stefan_text else "")) - else: - raise Exception("TICK fehlgeschlagen") + success = self.chat.send_message(tick_message) + + if success: + self.stats.ticks_sent += 1 + self.stats.consecutive_errors = 0 # Reset + logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if upload_images else "") + (f" + Stefan: {stefan_text[:30]}" if stefan_text else "")) + else: + raise Exception("TICK fehlgeschlagen") + finally: + # Senden fertig - wieder freigeben + self._sending.set() except Exception as e: logger.error(f"Heartbeat-Fehler: {e}") @@ -525,6 +548,7 @@ Erst dann starten die automatischen TICKs mit Bildern!""" Wenn Claude tippt → Buffer sammeln Wenn Claude fertig → Buffer wird mit nächstem TICK gesendet + Wenn gemutet → Ignoriert alle Eingaben So wird Claude nicht unterbrochen und bekommt alles gesammelt. """ @@ -536,9 +560,18 @@ Erst dann starten die automatischen TICKs mit Bildern!""" while self.running: try: + # Wenn gemutet, kurz warten und überspringen + if self.is_muted(): + time.sleep(0.5) + continue + # Warte auf Sprache (mit Timeout) result = self.stt.listen_once(timeout=2) + # Nochmal prüfen nach dem Hören (falls zwischendurch gemutet wurde) + if self.is_muted(): + continue + if result and result.text and len(result.text) > 2: # In Buffer speichern (thread-safe) with self._stefan_buffer_lock: @@ -554,6 +587,52 @@ Erst dann starten die automatischen TICKs mit Bildern!""" logger.error(f"STT-Loop-Fehler: {e}") self.stats.errors += 1 + def _keyboard_loop(self): + """ + Hört auf Tastatureingaben für Mute-Toggle. + + Tasten: + - M: Mute/Unmute Toggle + - Q: Beenden (alternativ zu Ctrl+C) + """ + import sys + import tty + import termios + + logger.info("Keyboard-Loop gestartet (M=Mute, Q=Quit)") + + # Speichere ursprüngliche Terminal-Settings + old_settings = None + try: + old_settings = termios.tcgetattr(sys.stdin) + except: + logger.warning("Konnte Terminal-Settings nicht lesen (kein TTY?)") + return + + try: + # Terminal in raw mode setzen (einzelne Tasten ohne Enter) + tty.setraw(sys.stdin.fileno()) + + while self.running: + # Lese einzelnes Zeichen (blockierend, aber mit select für Timeout) + import select + if select.select([sys.stdin], [], [], 0.5)[0]: + char = sys.stdin.read(1) + + if char.lower() == 'm': + self.toggle_mute() + elif char.lower() == 'q' or char == '\x03': # q oder Ctrl+C + console.print("\n[yellow]Beende Bridge...[/yellow]") + self.running = False + break + + except Exception as e: + logger.debug(f"Keyboard-Loop Fehler: {e}") + finally: + # Terminal-Settings wiederherstellen + if old_settings: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings) + def _get_and_clear_stefan_buffer(self) -> Optional[str]: """ Holt den Stefan-Buffer und leert ihn. @@ -571,6 +650,31 @@ Erst dann starten die automatischen TICKs mit Bildern!""" return text + def toggle_mute(self) -> bool: + """ + Schaltet Mute um. + + Returns: + True wenn jetzt gemutet, False wenn ungemutet + """ + with self._mute_lock: + self._muted = not self._muted + status = "MUTED" if self._muted else "UNMUTED" + console.print(f"\n[bold {'red' if self._muted else 'green'}]🎤 Mikrofon {status}[/bold {'red' if self._muted else 'green'}]") + return self._muted + + def set_mute(self, muted: bool): + """Setzt Mute-Status direkt""" + with self._mute_lock: + self._muted = muted + status = "MUTED" if self._muted else "UNMUTED" + console.print(f"\n[bold {'red' if self._muted else 'green'}]🎤 Mikrofon {status}[/bold {'red' if self._muted else 'green'}]") + + def is_muted(self) -> bool: + """Prüft ob gemutet""" + with self._mute_lock: + return self._muted + def _clean_for_speech(self, text: str) -> str: """ Entfernt Befehle und technische Teile aus dem Text.