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.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,48 +641,97 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
if not self.running:
break
# Warte bis Recording fertig ist (5s Stille)
# ════════════════════════════════════════════════════════════════
# 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("Claude fertig mit Sprechen")
if not self.running:
break
# ════════════════════════════════════════════════════════════════
# 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 noch, warte auf Stille...")
logger.debug("Stefan spricht, warte auf Stille...")
while self.running and self._recording.is_set():
time.sleep(0.5)
logger.debug("Stefan fertig, fahre fort")
logger.debug("Stefan fertig")
stefan_has_spoken = True
break
time.sleep(0.5)
if not self.running:
break
# Zufällige Pause nach Claudes Antwort (natürlicheres Tempo)
pause = random.uniform(min_pause, max_pause)
time.sleep(pause)
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
if stefan_text:
# ════════════════════════════════════════════════════════════
# 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 ''}")
self.chat.send_message(stefan_message)
self.stats.ticks_sent += 1
self.consecutive_errors = 0
logger.info(f"Stefan-Nachricht gesendet: {len(stefan_text)} Zeichen")
else:
# ════════════════════════════════════════════════════════════
# 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:
# 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:
@ -676,23 +741,19 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
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 senden
tick_message = "[TICK]"
console.print("[dim]→ TICK[/dim]")
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 ""))
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._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: