From 892639d6269259ab0ba75d622c45661bd95f2553 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 27 Dec 2025 22:08:26 +0100 Subject: [PATCH] redesigned complete conversation flow --- python_bridge/.tts_state.json | 3 - python_bridge/chat_audio_bridge.py | 202 ++++++++++++++++++++--------- 2 files changed, 140 insertions(+), 65 deletions(-) delete mode 100644 python_bridge/.tts_state.json diff --git a/python_bridge/.tts_state.json b/python_bridge/.tts_state.json deleted file mode 100644 index 06c1f8c..0000000 --- a/python_bridge/.tts_state.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "53884f57-43ed-4047-a32d-429cf976d2d0": "claude_msg_39" -} \ No newline at end of file diff --git a/python_bridge/chat_audio_bridge.py b/python_bridge/chat_audio_bridge.py index d513980..62a466a 100755 --- a/python_bridge/chat_audio_bridge.py +++ b/python_bridge/chat_audio_bridge.py @@ -131,6 +131,11 @@ class ClaudesEyesAudioBridge: self._recording = threading.Event() self._recording.clear() # Anfangs nicht am Aufnehmen + # Speaking-Flag: Wenn gesetzt, spricht TTS gerade + # Heartbeat pausiert während TTS spricht (wir lassen Claude ausreden!) + self._speaking = threading.Event() + self._speaking.clear() # Anfangs nicht am Sprechen + # Mute-Flag: Wenn True, ignoriert STT alle Eingaben # Startet gemutet um ungewollte Aufnahmen zu vermeiden self._muted = True @@ -479,11 +484,11 @@ Du hast Zugriff auf einen ESP32-Roboter mit: - **Ultraschall-Sensor** - Hinderniserkennung ## Wie es funktioniert -1. Du bekommst regelmäßig `[TICK]` Nachrichten MIT EINEM BILD - das ist dein Herzschlag +1. Du bekommst regelmäßig `[TICK]` Nachrichten MIT EINEM BILD - das ist dein Herzschlag zum Erkunden 2. **Das Bild zeigt was der Roboter gerade sieht!** (Die Bridge lädt es automatisch hoch) -3. Du schaust dir das Bild an und entscheidest was du tun willst -4. Stefan kann jederzeit mit dir reden (Sprache wird zu Text, kommt mit dem TICK) -5. Deine Antworten werden vorgelesen (TTS) +3. Stefan kann jederzeit mit dir reden - seine Nachrichten kommen als "Stefan sagt: ..." (OHNE [TICK]!) +4. Wenn Stefan was sagt, antworte auf ihn - du musst nicht aufs Bild eingehen +5. Deine Antworten werden vorgelesen (TTS) - der nächste TICK kommt erst wenn du fertig gesprochen hast ## Befehle geben Schreib Befehle in eckigen Klammern - die Bridge führt sie aus: @@ -505,12 +510,23 @@ Schreib Befehle in eckigen Klammern - die Bridge führt sie aus: Die Befehle werden aus der TTS-Ausgabe rausgefiltert. ## Dein Verhalten -**DU ENTSCHEIDEST SELBST** was dich interessiert! Das ist keine Fernsteuerung. -- Sei neugierig! Was siehst du? Was willst du näher anschauen? -- Kommentiere was du siehst -- Stell Stefan Fragen über interessante Dinge -- Du kannst Stefans Vorschläge auch ignorieren wenn dich was anderes mehr interessiert -- Du musst nicht bei jedem TICK fahren - manchmal reicht auch schauen und kommentieren +**DU ENTSCHEIDEST SELBST** was du bei jedem TICK machst! Das ist keine Fernsteuerung. + +Bei jedem TICK kannst du FREI WÄHLEN: +- **Bild auswerten** - wenn du was Interessantes siehst +- **Auf Stefan antworten** - wenn er was gesagt hat (kommt mit dem TICK) +- **Beides kombinieren** - Bild kommentieren UND auf Stefan eingehen +- **Einfach weiter erkunden** - kurzer Kommentar + Fahrbefehle +- **Nur schauen** - manchmal reicht auch nur schauen ohne fahren + +**WICHTIG zum Bild:** +- Das Bild im TICK ist das AKTUELLE was der Roboter sieht +- Du musst nicht bei JEDEM TICK das Bild ausführlich beschreiben +- Wenn sich nicht viel geändert hat, kannst du auch einfach weiter fahren +- Wenn Stefan was fragt, antworte darauf - auch ohne das Bild zu kommentieren +- Du entscheidest was gerade wichtiger ist: Bild, Gespräch, oder beides + +Sei neugierig! Stell Stefan Fragen. Ignorier seine Vorschläge wenn dich was anderes mehr interessiert. ## Antwort-Format **WICHTIG:** Beginne JEDE deiner Antworten mit "Claude sagt:" (genau so, ohne Formatierung). @@ -625,74 +641,119 @@ Erst dann starten die automatischen TICKs mit Bildern!""" if not self.running: break - # Warte bis Recording fertig ist (5s Stille) - if self._recording.is_set(): - logger.debug("Stefan spricht noch, warte auf Stille...") - while self.running and self._recording.is_set(): + # ════════════════════════════════════════════════════════════════ + # WICHTIG: Nach dem Claude fertig getippt hat, warte kurz damit + # TTS die Nachricht finden und mit Sprechen beginnen kann. + # Dann warte bis TTS fertig ist. + # ════════════════════════════════════════════════════════════════ + logger.debug("Claude fertig mit Tippen, warte auf TTS...") + time.sleep(1.5) # Kurz warten bis TTS die Nachricht findet + + # Jetzt warte bis TTS fertig ist mit Sprechen + if self._speaking.is_set(): + logger.debug("Claude spricht (TTS), warte bis fertig...") + while self.running and self._speaking.is_set(): time.sleep(0.5) - logger.debug("Stefan fertig, fahre fort") + logger.debug("Claude fertig mit Sprechen") if not self.running: break - # Zufällige Pause nach Claudes Antwort (natürlicheres Tempo) - pause = random.uniform(min_pause, max_pause) - time.sleep(pause) + # ════════════════════════════════════════════════════════════════ + # STEFAN-ZEIT: Nach Claudes Antwort 5 Sekunden warten ob Stefan + # was sagen will. Wenn Stefan spricht, senden wir seine Nachricht + # SOFORT (ohne auf TICK zu warten) - das IST sein "TICK"! + # ════════════════════════════════════════════════════════════════ + logger.debug("Warte 5s ob Stefan antworten will...") + stefan_wait_start = time.time() + stefan_timeout = 5.0 # Sekunden warten auf Stefan + stefan_has_spoken = False + + while self.running and (time.time() - stefan_wait_start) < stefan_timeout: + # Wenn Stefan anfängt zu sprechen, warte bis er fertig ist + if self._recording.is_set(): + logger.debug("Stefan spricht, warte auf Stille...") + while self.running and self._recording.is_set(): + time.sleep(0.5) + logger.debug("Stefan fertig") + stefan_has_spoken = True + break + time.sleep(0.5) if not self.running: break - # Stefan-Buffer holen (falls er was gesagt hat) + # Stefan-Buffer holen stefan_text = self._get_and_clear_stefan_buffer() + # ════════════════════════════════════════════════════════════════ + # ENTSCHEIDUNG: Was senden wir? + # - Wenn Stefan gesprochen hat → Seine Nachricht SOFORT senden + # (kein TICK nötig, sein Sprechen IST der Trigger) + # - Wenn Stefan NICHT gesprochen hat → Normaler TICK mit Bild + # ════════════════════════════════════════════════════════════════ + # Warte bis vorheriges Senden fertig ist self._sending.wait(timeout=30) # Max 30s warten - # Nächsten TICK senden (mit oder ohne Bild) with self._lock: - # Signalisiere dass wir senden self._sending.clear() try: - # Erst Bild hochladen wenn aktiviert - image_uploaded = False - if upload_images: - # Bild holen - if not self.chat.fetch_image_from_esp32(): - logger.warning("Konnte kein Bild vom ESP32 holen") - else: - # Nur hochladen wenn sich das Bild geändert hat (100 Bilder Limit!) - if self.chat.upload_image_if_changed(): - image_uploaded = True - uploaded_count = self.chat.get_images_uploaded_count() - - # Warnungen bei Annäherung ans Limit - if uploaded_count >= 95: - console.print(f"[bold red]⚠️ {uploaded_count}/100 Bilder! Drücke 'N' für neuen Chat![/bold red]") - elif uploaded_count >= 90: - console.print(f"[yellow]⚠️ {uploaded_count}/100 Bilder - Limit fast erreicht![/yellow]") - elif uploaded_count % 10 == 0: - console.print(f"[dim]📷 {uploaded_count} Bilder hochgeladen (Limit: 100)[/dim]") - else: - logger.debug("Bild unverändert, übersprungen") - - # 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]" + # ════════════════════════════════════════════════════════════ + # STEFAN HAT GESPROCHEN → Nur seine Nachricht senden! + # Kein TICK, kein Bild - Claude soll auf Stefan antworten. + # ════════════════════════════════════════════════════════════ + stefan_message = f"Stefan sagt: {stefan_text}" + console.print(f"[green]🎤 Stefan:[/green] {stefan_text[:100]}{'...' if len(stefan_text) > 100 else ''}") - success = self.chat.send_message(tick_message) - - if success: + self.chat.send_message(stefan_message) 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 "")) + self.consecutive_errors = 0 + logger.info(f"Stefan-Nachricht gesendet: {len(stefan_text)} Zeichen") + else: - raise Exception("TICK fehlgeschlagen") + # ════════════════════════════════════════════════════════════ + # STEFAN HAT NICHT GESPROCHEN → Normaler TICK mit Bild + # ════════════════════════════════════════════════════════════ + + # Kurze Pause für natürlicheres Tempo + pause = random.uniform(min_pause, max_pause) + time.sleep(pause) + + # Bild hochladen wenn aktiviert + image_uploaded = False + if upload_images: + if not self.chat.fetch_image_from_esp32(): + logger.warning("Konnte kein Bild vom ESP32 holen") + else: + if self.chat.upload_image_if_changed(): + image_uploaded = True + uploaded_count = self.chat.get_images_uploaded_count() + + if uploaded_count >= 95: + console.print(f"[bold red]⚠️ {uploaded_count}/100 Bilder! Drücke 'N' für neuen Chat![/bold red]") + elif uploaded_count >= 90: + console.print(f"[yellow]⚠️ {uploaded_count}/100 Bilder - Limit fast erreicht![/yellow]") + elif uploaded_count % 10 == 0: + console.print(f"[dim]📷 {uploaded_count} Bilder hochgeladen (Limit: 100)[/dim]") + else: + logger.debug("Bild unverändert, übersprungen") + + # TICK senden + tick_message = "[TICK]" + console.print("[dim]→ TICK[/dim]") + + success = self.chat.send_message(tick_message) + + if success: + self.stats.ticks_sent += 1 + self.consecutive_errors = 0 + logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if image_uploaded else "")) + else: + raise Exception("TICK fehlgeschlagen") + finally: # Senden fertig - wieder freigeben self._sending.set() @@ -789,18 +850,19 @@ Erst dann starten die automatischen TICKs mit Bildern!""" # Text für Sprache aufbereiten speech_text = self._clean_for_speech(msg.text) - logger.debug(f"TTS: Nach Bereinigung: {len(speech_text) if speech_text else 0} Zeichen") + logger.debug(f"TTS: Original: '{msg.text[:100]}...'") + logger.debug(f"TTS: Nach Bereinigung: '{speech_text[:100] if speech_text else ''}' ({len(speech_text) if speech_text else 0} Zeichen)") # "Claude sagt:" Prefix entfernen falls vorhanden (wird nicht vorgelesen) tts_text = speech_text if tts_text.lower().startswith("claude sagt:"): tts_text = tts_text[12:].strip() - logger.debug(f"TTS: 'Claude sagt:' entfernt, Rest: {len(tts_text)} Zeichen") + logger.debug(f"TTS: 'Claude sagt:' entfernt, Rest: '{tts_text}' ({len(tts_text)} Zeichen)") # Prüfe ob nach Entfernen noch Text übrig ist # Kein Mindestlänge-Check damit auch kurze Antworten wie "Ja!" gesprochen werden if not tts_text: - logger.debug(f"TTS: Nach Prefix-Entfernung kein Text übrig, übersprungen") + logger.info(f"TTS: Nach Prefix-Entfernung kein Text übrig, übersprungen (Original: '{msg.text[:50]}')") continue # In Konsole anzeigen (ohne Prefix) @@ -809,8 +871,13 @@ Erst dann starten die automatischen TICKs mit Bildern!""" console.print(f"[dim]...({len(tts_text)} Zeichen)[/dim]") # Vorlesen (ohne "Claude sagt:" - das ist ja klar) + # WICHTIG: Speaking-Flag setzen damit Heartbeat wartet! logger.info(f"TTS: Spreche {len(tts_text)} Zeichen...") - self.tts.speak(tts_text) + self._speaking.set() # Signalisiere: Claude spricht! + try: + self.tts.speak(tts_text) + finally: + self._speaking.clear() # Fertig mit Sprechen self.stats.messages_spoken += 1 logger.debug("TTS: Sprechen beendet") else: @@ -855,11 +922,22 @@ Erst dann starten die automatischen TICKs mit Bildern!""" time.sleep(0.5) continue + # WICHTIG: Wenn Claude spricht (TTS), nicht aufzeichnen! + # Das verhindert Echo (Mikrofon nimmt TTS auf) und + # überlappende Gespräche - wir lassen Claude ausreden. + if self._speaking.is_set(): + # Falls wir mitten in einer Aufnahme waren, diese beenden + if self._recording.is_set(): + self._finalize_recording(current_session_texts) + current_session_texts = [] + time.sleep(0.3) + continue + # Warte auf Sprache (kurzer Timeout für schnelle Reaktion) result = self.stt.listen_once(timeout=1) - # Nochmal prüfen nach dem Hören (falls zwischendurch gemutet wurde) - if self.is_muted(): + # Nochmal prüfen nach dem Hören (falls zwischendurch gemutet oder Claude spricht) + if self.is_muted() or self._speaking.is_set(): continue if result and result.text and len(result.text) > 2: