redesigned complete conversation flow

This commit is contained in:
duffyduck 2025-12-27 22:08:26 +01:00
parent e7a62efe90
commit 892639d626
2 changed files with 140 additions and 65 deletions

View File

@ -1,3 +0,0 @@
{
"53884f57-43ed-4047-a32d-429cf976d2d0": "claude_msg_39"
}

View File

@ -131,6 +131,11 @@ class ClaudesEyesAudioBridge:
self._recording = threading.Event() self._recording = threading.Event()
self._recording.clear() # Anfangs nicht am Aufnehmen 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 # Mute-Flag: Wenn True, ignoriert STT alle Eingaben
# Startet gemutet um ungewollte Aufnahmen zu vermeiden # Startet gemutet um ungewollte Aufnahmen zu vermeiden
self._muted = True self._muted = True
@ -479,11 +484,11 @@ Du hast Zugriff auf einen ESP32-Roboter mit:
- **Ultraschall-Sensor** - Hinderniserkennung - **Ultraschall-Sensor** - Hinderniserkennung
## Wie es funktioniert ## 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) 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 3. Stefan kann jederzeit mit dir reden - seine Nachrichten kommen als "Stefan sagt: ..." (OHNE [TICK]!)
4. Stefan kann jederzeit mit dir reden (Sprache wird zu Text, kommt mit dem TICK) 4. Wenn Stefan was sagt, antworte auf ihn - du musst nicht aufs Bild eingehen
5. Deine Antworten werden vorgelesen (TTS) 5. Deine Antworten werden vorgelesen (TTS) - der nächste TICK kommt erst wenn du fertig gesprochen hast
## Befehle geben ## Befehle geben
Schreib Befehle in eckigen Klammern - die Bridge führt sie aus: 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. Die Befehle werden aus der TTS-Ausgabe rausgefiltert.
## Dein Verhalten ## Dein Verhalten
**DU ENTSCHEIDEST SELBST** was dich interessiert! Das ist keine Fernsteuerung. **DU ENTSCHEIDEST SELBST** was du bei jedem TICK machst! Das ist keine Fernsteuerung.
- Sei neugierig! Was siehst du? Was willst du näher anschauen?
- Kommentiere was du siehst Bei jedem TICK kannst du FREI WÄHLEN:
- Stell Stefan Fragen über interessante Dinge - **Bild auswerten** - wenn du was Interessantes siehst
- Du kannst Stefans Vorschläge auch ignorieren wenn dich was anderes mehr interessiert - **Auf Stefan antworten** - wenn er was gesagt hat (kommt mit dem TICK)
- Du musst nicht bei jedem TICK fahren - manchmal reicht auch schauen und kommentieren - **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 ## Antwort-Format
**WICHTIG:** Beginne JEDE deiner Antworten mit "Claude sagt:" (genau so, ohne Formatierung). **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: if not self.running:
break break
# Warte bis Recording fertig ist (5s Stille) # ════════════════════════════════════════════════════════════════
if self._recording.is_set(): # WICHTIG: Nach dem Claude fertig getippt hat, warte kurz damit
logger.debug("Stefan spricht noch, warte auf Stille...") # TTS die Nachricht finden und mit Sprechen beginnen kann.
while self.running and self._recording.is_set(): # 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) time.sleep(0.5)
logger.debug("Stefan fertig, fahre fort") logger.debug("Claude fertig mit Sprechen")
if not self.running: if not self.running:
break break
# Zufällige Pause nach Claudes Antwort (natürlicheres Tempo) # ════════════════════════════════════════════════════════════════
pause = random.uniform(min_pause, max_pause) # STEFAN-ZEIT: Nach Claudes Antwort 5 Sekunden warten ob Stefan
time.sleep(pause) # 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: if not self.running:
break break
# Stefan-Buffer holen (falls er was gesagt hat) # Stefan-Buffer holen
stefan_text = self._get_and_clear_stefan_buffer() 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 # Warte bis vorheriges Senden fertig ist
self._sending.wait(timeout=30) # Max 30s warten self._sending.wait(timeout=30) # Max 30s warten
# Nächsten TICK senden (mit oder ohne Bild)
with self._lock: with self._lock:
# Signalisiere dass wir senden
self._sending.clear() self._sending.clear()
try: 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: if stefan_text:
# Stefan hat was gesagt → Mit TICK senden # ════════════════════════════════════════════════════════════
tick_message = f"[TICK]\n\nStefan sagt: {stefan_text}" # STEFAN HAT GESPROCHEN → Nur seine Nachricht senden!
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]") # Kein TICK, kein Bild - Claude soll auf Stefan antworten.
else: # ════════════════════════════════════════════════════════════
# Nur TICK stefan_message = f"Stefan sagt: {stefan_text}"
tick_message = "[TICK]" console.print(f"[green]🎤 Stefan:[/green] {stefan_text[:100]}{'...' if len(stefan_text) > 100 else ''}")
success = self.chat.send_message(tick_message) self.chat.send_message(stefan_message)
if success:
self.stats.ticks_sent += 1 self.stats.ticks_sent += 1
self.stats.consecutive_errors = 0 # Reset self.consecutive_errors = 0
logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if upload_images else "") + (f" + Stefan: {stefan_text[:30]}" if stefan_text else "")) logger.info(f"Stefan-Nachricht gesendet: {len(stefan_text)} Zeichen")
else: 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: finally:
# Senden fertig - wieder freigeben # Senden fertig - wieder freigeben
self._sending.set() self._sending.set()
@ -789,18 +850,19 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
# Text für Sprache aufbereiten # Text für Sprache aufbereiten
speech_text = self._clean_for_speech(msg.text) 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) # "Claude sagt:" Prefix entfernen falls vorhanden (wird nicht vorgelesen)
tts_text = speech_text tts_text = speech_text
if tts_text.lower().startswith("claude sagt:"): if tts_text.lower().startswith("claude sagt:"):
tts_text = tts_text[12:].strip() 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 # Prüfe ob nach Entfernen noch Text übrig ist
# Kein Mindestlänge-Check damit auch kurze Antworten wie "Ja!" gesprochen werden # Kein Mindestlänge-Check damit auch kurze Antworten wie "Ja!" gesprochen werden
if not tts_text: 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 continue
# In Konsole anzeigen (ohne Prefix) # 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]") console.print(f"[dim]...({len(tts_text)} Zeichen)[/dim]")
# Vorlesen (ohne "Claude sagt:" - das ist ja klar) # Vorlesen (ohne "Claude sagt:" - das ist ja klar)
# WICHTIG: Speaking-Flag setzen damit Heartbeat wartet!
logger.info(f"TTS: Spreche {len(tts_text)} Zeichen...") 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 self.stats.messages_spoken += 1
logger.debug("TTS: Sprechen beendet") logger.debug("TTS: Sprechen beendet")
else: else:
@ -855,11 +922,22 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
time.sleep(0.5) time.sleep(0.5)
continue 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) # Warte auf Sprache (kurzer Timeout für schnelle Reaktion)
result = self.stt.listen_once(timeout=1) result = self.stt.listen_once(timeout=1)
# Nochmal prüfen nach dem Hören (falls zwischendurch gemutet wurde) # Nochmal prüfen nach dem Hören (falls zwischendurch gemutet oder Claude spricht)
if self.is_muted(): if self.is_muted() or self._speaking.is_set():
continue continue
if result and result.text and len(result.text) > 2: if result and result.text and len(result.text) > 2: