diff --git a/README.md b/README.md index 89710a3..b8d0d6a 100644 --- a/README.md +++ b/README.md @@ -166,8 +166,9 @@ Claude verwendet diese Befehle in eckigen Klammern: - **Echte Autonomie** - Claude entscheidet selbst was ihn interessiert - **Paralelle Konversation** - Erkunden UND quatschen gleichzeitig - **Sprachausgabe** - Claude redet mit dir (TTS) -- **Spracheingabe** - Du redest mit Claude (STT) +- **Spracheingabe** - Du redest mit Claude (STT, 5s Stille = fertig) - **Mute/Unmute** - Mikrofon per Tastendruck stummschalten +- **Smart Recording** - Heartbeat pausiert automatisch während du sprichst - **Hinderniserkennung** - Ultraschall & IMU - **Touch-Display** - Notfall-Stopp & Status - **Termux Support** - Läuft auch auf Android! diff --git a/docs/setup_guide.md b/docs/setup_guide.md index 15943de..fcbd78c 100644 --- a/docs/setup_guide.md +++ b/docs/setup_guide.md @@ -245,6 +245,31 @@ python chat_audio_bridge.py -c config.local.yaml - Mit **N** startest du einen neuen Chat und die Instruktionen werden erneut gesendet - Bilder werden nur hochgeladen wenn sie sich geändert haben (spart Limit!) +### 2.7 Spracheingabe (STT) - Wie es funktioniert + +Die Spracheingabe sammelt deine Worte intelligent: + +1. **Aufnahme startet** sobald du sprichst +2. **Heartbeat pausiert** automatisch während du redest +3. **Mehrere Sätze** werden gesammelt und zusammengefügt +4. **Nach 5 Sekunden Stille** gilt die Eingabe als komplett +5. **Mit dem nächsten TICK** wird alles an Claude gesendet + +**Beispiel-Ablauf:** +``` +Du: "Hallo Claude..." → 🎤 Stefan spricht... +Du: "...kannst du mal..." → → Hallo Claude +Du: "...nach links schauen?" → → kannst du mal + → → nach links schauen? +[5 Sekunden Stille] → ✓ Stefan (komplett): Hallo Claude kannst du mal nach links schauen? +[TICK wird gesendet] → Claude bekommt die komplette Nachricht +``` + +**Warum 5 Sekunden?** +- Gibt dir Zeit zum Nachdenken zwischen Sätzen +- Verhindert dass halbe Sätze gesendet werden +- Claude bekommt immer den kompletten Gedanken + --- ## Teil 3: Hardware zusammenbauen diff --git a/python_bridge/chat_audio_bridge.py b/python_bridge/chat_audio_bridge.py index 6764446..8b8bf97 100755 --- a/python_bridge/chat_audio_bridge.py +++ b/python_bridge/chat_audio_bridge.py @@ -120,11 +120,19 @@ class ClaudesEyesAudioBridge: self._sending = threading.Event() self._sending.set() # Anfangs nicht am Senden (set = frei) + # Recording-Flag: Wenn gesetzt, wird gerade aufgenommen + # Heartbeat pausiert während Aufnahme aktiv ist + self._recording = threading.Event() + self._recording.clear() # Anfangs nicht am Aufnehmen + # Mute-Flag: Wenn True, ignoriert STT alle Eingaben # Startet gemutet um ungewollte Aufnahmen zu vermeiden self._muted = True self._mute_lock = threading.Lock() + # Silence-Timeout: Wie lange Stille bevor Aufnahme als fertig gilt + self._silence_timeout = 5.0 # Sekunden + def _load_config(self, config_path: str) -> dict: """Lädt die Konfiguration""" path = Path(config_path) @@ -428,7 +436,8 @@ Die Befehle werden aus der TTS-Ausgabe rausgefiltert. - Du musst nicht bei jedem TICK fahren - manchmal reicht auch schauen und kommentieren ## WICHTIG: Bestätige mit [READY] -Wenn du diese Instruktionen verstanden hast, antworte mit **[READY]** am Ende deiner Nachricht. +Wenn du diese Instruktionen verstanden hast, antworte mit dem Tag `[READY]` (exakt so, in eckigen Klammern!) am Ende deiner Nachricht. +**Das Format muss EXAKT `[READY]` sein** - nicht fett, nicht anders formatiert, sondern genau so: [READY] Erst dann starten die automatischen TICKs mit Bildern!""" console.print("[cyan]→ Sende Instruktionen an Claude...[/cyan]") @@ -500,6 +509,16 @@ 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(): + time.sleep(0.5) + logger.debug("Stefan fertig, fahre fort") + + if not self.running: + break + # Zufällige Pause nach Claudes Antwort (natürlicheres Tempo) pause = random.uniform(min_pause, max_pause) time.sleep(pause) @@ -511,7 +530,7 @@ Erst dann starten die automatischen TICKs mit Bildern!""" stefan_text = self._get_and_clear_stefan_buffer() # Warte bis vorheriges Senden fertig ist - self._sending.wait() + self._sending.wait(timeout=30) # Max 30s warten # Nächsten TICK senden (mit oder ohne Bild) with self._lock: @@ -622,40 +641,67 @@ Erst dann starten die automatischen TICKs mit Bildern!""" """ Hört auf Stefan und sammelt seine Worte im Buffer. - Wenn Claude tippt → Buffer sammeln - Wenn Claude fertig → Buffer wird mit nächstem TICK gesendet - Wenn gemutet → Ignoriert alle Eingaben + Ablauf: + 1. Warte auf erste Spracheingabe + 2. Signalisiere Recording aktiv → Heartbeat pausiert + 3. Sammle weitere Eingaben bis 5 Sekunden Stille + 4. Recording fertig → Buffer wird mit nächstem TICK gesendet - So wird Claude nicht unterbrochen und bekommt alles gesammelt. + Wenn gemutet → Ignoriert alle Eingaben """ if not self.stt: logger.warning("STT nicht verfügbar") return - logger.info("STT-Loop gestartet (mit Buffer)") + logger.info(f"STT-Loop gestartet (5s Stille = fertig)") + + # Temporärer Buffer für aktuelle Aufnahme-Session + current_session_texts = [] + last_speech_time = 0 while self.running: try: # Wenn gemutet, kurz warten und überspringen if self.is_muted(): + # 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.5) continue - # Warte auf Sprache (mit Timeout) - result = self.stt.listen_once(timeout=2) + # 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(): continue if result and result.text and len(result.text) > 2: - # In Buffer speichern (thread-safe) - with self._stefan_buffer_lock: - self._stefan_buffer.append(result.text) - self.stats.stefan_inputs += 1 + # Sprache erkannt! + current_session_texts.append(result.text) + last_speech_time = time.time() + self.stats.stefan_inputs += 1 - console.print(f"\n[bold green]Stefan (gebuffert):[/bold green] {result.text}") - logger.debug(f"Stefan-Buffer: {len(self._stefan_buffer)} Einträge") + # Signalisiere dass Recording aktiv ist + if not self._recording.is_set(): + self._recording.set() + console.print(f"\n[bold green]🎤 Stefan spricht...[/bold green]") + logger.debug("Recording gestartet") + + console.print(f"[green] → {result.text}[/green]") + logger.debug(f"Stefan-Session: {len(current_session_texts)} Teile") + + else: + # Keine Sprache erkannt - prüfe auf Stille-Timeout + if self._recording.is_set() and last_speech_time > 0: + silence_duration = time.time() - last_speech_time + + if silence_duration >= self._silence_timeout: + # 5 Sekunden Stille - Aufnahme beenden + self._finalize_recording(current_session_texts) + current_session_texts = [] + last_speech_time = 0 except Exception as e: # Timeout ist normal @@ -663,6 +709,30 @@ Erst dann starten die automatischen TICKs mit Bildern!""" logger.error(f"STT-Loop-Fehler: {e}") self.stats.errors += 1 + def _finalize_recording(self, texts: list): + """ + Beendet eine Aufnahme-Session und speichert im Buffer. + + Args: + texts: Liste der erkannten Texte in dieser Session + """ + if not texts: + self._recording.clear() + return + + # Alle Texte zusammenfügen + full_text = " ".join(texts) + + # In Haupt-Buffer speichern + with self._stefan_buffer_lock: + self._stefan_buffer.append(full_text) + + console.print(f"\n[bold green]✓ Stefan (komplett):[/bold green] {full_text}") + logger.info(f"Recording beendet: {len(texts)} Teile → {len(full_text)} Zeichen") + + # Recording-Flag zurücksetzen → Heartbeat kann weitermachen + self._recording.clear() + def _keyboard_loop(self): """ Hört auf Tastatureingaben für Mute-Toggle und andere Befehle. @@ -730,6 +800,10 @@ Erst dann starten die automatischen TICKs mit Bildern!""" # Neue URL in config.yaml speichern self._save_chat_url_to_config(new_url) + # Warte bis der neue Chat vollständig geladen ist + console.print("[dim]Warte 10s bis Chat geladen...[/dim]") + time.sleep(10) + console.print("[cyan]Sende Instruktionen mit Referenz zum alten Chat...[/cyan]") # Instruktionen erneut senden (mit Referenz zum alten Chat)