diff --git a/python_bridge/chat_audio_bridge.py b/python_bridge/chat_audio_bridge.py index bbfc4b3..3b082af 100755 --- a/python_bridge/chat_audio_bridge.py +++ b/python_bridge/chat_audio_bridge.py @@ -109,9 +109,13 @@ class ClaudesEyesAudioBridge: self.last_assistant_message_id: Optional[str] = None self._lock = threading.Lock() - # Ready-Flag: Heartbeat wartet bis Claude [READY] gesendet hat + # Ready-Flag: Heartbeat UND TTS warten bis Claude [READY] gesendet hat self._claude_ready = threading.Event() + # TTS-Active-Flag: TTS wird erst aktiviert nachdem [READY] empfangen UND + # die letzte Nachricht-ID gesetzt wurde (um alte Nachrichten zu ignorieren) + self._tts_active = threading.Event() + # Stefan-Buffer: Sammelt Spracheingaben während Claude tippt self._stefan_buffer: list = [] self._stefan_buffer_lock = threading.Lock() @@ -467,8 +471,23 @@ Erst dann starten die automatischen TICKs mit Bildern!""" # Warte auf [READY] - KEIN Timeout-Fallback! # Heartbeat startet NUR wenn Claude wirklich [READY] sendet if self.chat.wait_for_ready_signal(timeout=300): # 5 Minuten max - # Signal für Heartbeat dass es losgehen kann + # ════════════════════════════════════════════════════════════════ + # WICHTIG: Markiere die letzte Nachricht BEVOR TTS aktiviert wird + # So werden alle bisherigen Nachrichten (inkl. [READY]) ignoriert + # ════════════════════════════════════════════════════════════════ + last_msg = self.chat.get_last_assistant_message() + if last_msg: + self.last_assistant_message_id = last_msg.id + logger.info(f"TTS startet nach Nachricht: {last_msg.id[:30]}...") + else: + # Fallback: Generiere eine Dummy-ID die niemals matcht + self.last_assistant_message_id = f"init_{time.time()}" + logger.info("Keine letzte Nachricht gefunden, TTS startet frisch") + + # Jetzt TTS und Heartbeat aktivieren + self._tts_active.set() self._claude_ready.set() + console.print("[green]TTS aktiviert - ab jetzt werden neue Nachrichten vorgelesen[/green]") return True else: # KEIN Fallback - Heartbeat bleibt blockiert @@ -619,12 +638,21 @@ Erst dann starten die automatischen TICKs mit Bildern!""" Filtert dabei [BEFEHLE] und technische Teile raus, sodass nur der "menschliche" Text gesprochen wird. + + WICHTIG: Wartet auf _tts_active bevor Nachrichten vorgelesen werden! + So werden Init-Nachrichten und UI-Texte ignoriert. """ if not self.tts: logger.warning("TTS nicht verfügbar") return - logger.info("TTS-Loop gestartet") + logger.info("TTS-Loop gestartet, warte auf READY...") + + # ════════════════════════════════════════════════════════════════ + # WICHTIG: Warte bis TTS aktiviert wird (nach [READY]) + # ════════════════════════════════════════════════════════════════ + self._tts_active.wait() + logger.info("TTS aktiviert - beginne mit Vorlesen neuer Nachrichten") while self.running: try: @@ -818,8 +846,9 @@ Erst dann starten die automatischen TICKs mit Bildern!""" """Startet einen neuen Chat und sendet die Instruktionen erneut""" console.print("\n[yellow]Starte neuen Chat...[/yellow]") - # Heartbeat pausieren + # Heartbeat UND TTS pausieren self._claude_ready.clear() + self._tts_active.clear() with self._lock: new_url, old_chat_id = self.chat.start_new_chat() diff --git a/python_bridge/chat_web_interface.py b/python_bridge/chat_web_interface.py index 0318bec..c45e2b9 100644 --- a/python_bridge/chat_web_interface.py +++ b/python_bridge/chat_web_interface.py @@ -526,142 +526,122 @@ class ClaudeChatInterface: return new_messages def _get_all_messages(self) -> List[ChatMessage]: - """Holt alle Nachrichten aus dem Chat""" + """ + Holt alle Nachrichten aus dem Chat. + + Claude.ai verwendet unterschiedliche data-testid für User und Assistant: + - data-testid="user-message" für User + - Für Claude: Wir suchen nach Nachrichten die NICHT user-message sind + """ messages = [] try: - # Versuche verschiedene Selektoren - elements = [] + # ═══════════════════════════════════════════════════════════════ + # STRATEGIE: Hole User UND Assistant Nachrichten SEPARAT + # ═══════════════════════════════════════════════════════════════ - # Claude.ai spezifische Selektoren (2024/2025) - message_selectors = [ - # Neuere Claude.ai Versionen - "[data-testid='user-message']", - "[data-testid='assistant-message']", - # Ältere Varianten - "div[data-is-streaming='false']", - # Container für Nachrichten-Inhalt - ".font-claude-message", - ".prose", - # Generische Fallbacks - "div[class*='message']", - "div[class*='Message']", - ] + # 1. User-Nachrichten (haben data-testid="user-message") + user_elements = [] + try: + user_elements = self.driver.find_elements( + By.CSS_SELECTOR, + "[data-testid='user-message']" + ) + logger.debug(f"User-Nachrichten gefunden: {len(user_elements)}") + except Exception as e: + logger.debug(f"User-Nachrichten Suche fehlgeschlagen: {e}") - for selector in message_selectors: + # 2. Claude-Nachrichten suchen + # Claude.ai nutzt verschiedene Klassen, aber NICHT user-message + claude_elements = [] + + # Methode A: font-claude-message Klasse + try: + found = self.driver.find_elements( + By.CSS_SELECTOR, + "[class*='font-claude-message']" + ) + if found: + claude_elements = found + logger.debug(f"Claude-Nachrichten via font-claude-message: {len(found)}") + except: + pass + + # Methode B: Prose-Container die NICHT in user-message sind + if not claude_elements: try: - found = self.driver.find_elements(By.CSS_SELECTOR, selector) - if found: - logger.debug(f"Selektor '{selector}' fand {len(found)} Elemente") - if not elements or len(found) > len(elements): - elements = found - except Exception as e: - logger.debug(f"Selektor '{selector}' fehlgeschlagen: {e}") - - logger.debug(f"Gesamt gefundene Nachrichten-Elemente: {len(elements)}") - - # Wenn immer noch nichts gefunden, versuche via JavaScript - if not elements: - try: - elements = self.driver.execute_script(""" - // Suche nach Nachrichten-Containern + # Alle prose Elemente die nicht innerhalb von user-message sind + found = self.driver.execute_script(""" const msgs = []; - - // Methode 1: data-testid - document.querySelectorAll('[data-testid*="message"]').forEach(e => msgs.push(e)); - - // Methode 2: Prose-Container (Markdown-Inhalt) - if (msgs.length === 0) { - document.querySelectorAll('.prose').forEach(e => msgs.push(e)); - } - - // Methode 3: Alle großen Text-Container im Hauptbereich - if (msgs.length === 0) { - document.querySelectorAll('main div').forEach(e => { - if (e.innerText && e.innerText.length > 50 && e.children.length < 10) { - msgs.push(e); + document.querySelectorAll('.prose').forEach(e => { + // Prüfe ob dieses Element NICHT in einem user-message Container ist + let parent = e; + let isUserMessage = false; + while (parent && parent !== document.body) { + if (parent.getAttribute('data-testid') === 'user-message') { + isUserMessage = true; + break; } - }); - } - + parent = parent.parentElement; + } + if (!isUserMessage) { + msgs.push(e); + } + }); return msgs; """) or [] - logger.debug(f"JavaScript fand {len(elements)} Nachrichten") + if found: + claude_elements = found + logger.debug(f"Claude-Nachrichten via prose (nicht-user): {len(found)}") except Exception as e: - logger.debug(f"JavaScript-Suche fehlgeschlagen: {e}") + logger.debug(f"Prose-Suche fehlgeschlagen: {e}") - for i, elem in enumerate(elements): + # ═══════════════════════════════════════════════════════════════ + # Nachrichten verarbeiten + # ═══════════════════════════════════════════════════════════════ + + # User-Nachrichten hinzufügen + for i, elem in enumerate(user_elements): + try: + text = elem.text.strip() + if not text or len(text) < 3: + continue + + msg_id = elem.get_attribute("data-message-id") + if not msg_id: + msg_id = f"user_{i}_{hash(text[:100])}" + + messages.append(ChatMessage( + id=msg_id, + text=text, + is_from_assistant=False, + timestamp=time.time() + )) + except Exception as e: + logger.debug(f"Fehler bei User-Nachricht {i}: {e}") + + # Claude-Nachrichten hinzufügen + for i, elem in enumerate(claude_elements): try: text = elem.text.strip() if not text or len(text) < 5: continue - # Bestimme ob Human oder Assistant mit mehreren Methoden - class_name = elem.get_attribute("class") or "" - data_testid = elem.get_attribute("data-testid") or "" - data_role = elem.get_attribute("data-role") or "" - - # Prüfe Parent-Elemente auf Hinweise - parent_hints = "" - try: - parent = elem.find_element(By.XPATH, "..") - parent_class = parent.get_attribute("class") or "" - parent_testid = parent.get_attribute("data-testid") or "" - grandparent = parent.find_element(By.XPATH, "..") - grandparent_class = grandparent.get_attribute("class") or "" - parent_hints = f"{parent_class} {parent_testid} {grandparent_class}" - except: - pass - - # Kombiniere alle Hinweise - all_hints = (class_name + " " + data_testid + " " + data_role + " " + parent_hints).lower() - - # Prüfe explizit auf "user" oder "human" für User-Nachrichten - is_human = ( - "user" in all_hints or - "human" in all_hints or - "user-message" in data_testid - ) - - is_assistant = ( - "assistant" in all_hints or - "claude" in all_hints or - "ai-message" in all_hints or - "assistant-message" in data_testid or - "response" in all_hints - ) - - # Wenn weder user noch assistant explizit, nutze Heuristik - if not is_human and not is_assistant: - # Prüfe ob Text typische User-Marker hat - if text.startswith("[TICK]") or text.startswith("[START]") or "Stefan sagt:" in text: - is_human = True - else: - # Fallback: Nachrichten ohne User-Marker sind von Claude - is_assistant = True - - # Final: is_assistant ist True wenn nicht explizit Human - final_is_assistant = is_assistant and not is_human - - logger.debug(f"Nachricht {i}: is_human={is_human}, is_assistant={is_assistant}, final={final_is_assistant}, hints='{all_hints[:80]}...', text='{text[:40]}...'") - - # Generiere ID msg_id = elem.get_attribute("data-message-id") if not msg_id: - msg_id = f"msg_{i}_{hash(text[:100])}" + msg_id = f"claude_{i}_{hash(text[:100])}" messages.append(ChatMessage( id=msg_id, text=text, - is_from_assistant=final_is_assistant, + is_from_assistant=True, timestamp=time.time() )) - + logger.debug(f"Claude-Nachricht {i}: '{text[:50]}...'") except Exception as e: - logger.debug(f"Fehler bei Nachricht {i}: {e}") - continue + logger.debug(f"Fehler bei Claude-Nachricht {i}: {e}") - logger.debug(f"Parsed {len(messages)} Nachrichten, davon {sum(1 for m in messages if m.is_from_assistant)} von Claude") + logger.debug(f"Gesamt: {len(messages)} Nachrichten, davon {sum(1 for m in messages if m.is_from_assistant)} von Claude") except Exception as e: logger.error(f"Fehler beim Lesen der Nachrichten: {e}")