neuer chat auf grund 100 dateien in chat implementiert und check ob bilder doppelt

This commit is contained in:
duffyduck 2025-12-27 02:42:34 +01:00
parent 095385a4a5
commit 90791b9ff6
4 changed files with 355 additions and 12 deletions

View File

@ -177,9 +177,12 @@ Claude verwendet diese Befehle in eckigen Klammern:
| Taste | Funktion |
|-------|----------|
| **M** | Mikrofon Mute/Unmute |
| **N** | Neuer Chat (bei 100-Bilder-Limit) |
| **Q** | Bridge beenden |
| **Ctrl+C** | Bridge beenden |
**Hinweis:** Claude.ai erlaubt max. 100 Bilder pro Chat. Die Bridge warnt bei 90/95 Bildern. Mit **N** startest du einen neuen Chat.
---
## Sicherheit

View File

@ -234,10 +234,16 @@ python chat_audio_bridge.py -c config.local.yaml
| Taste | Funktion |
|-------|----------|
| **M** | Mikrofon Mute/Unmute - schaltet STT stumm |
| **N** | Neuer Chat starten (bei Bilder-Limit) |
| **Q** | Bridge beenden |
| **Ctrl+C** | Bridge beenden |
**Tipp:** Nutze Mute wenn du nicht willst dass Hintergrundgeräusche oder Gespräche mit anderen aufgenommen werden. Das Mikrofon nimmt sonst alles auf!
**Tipps:**
- Nutze **Mute** wenn du nicht willst dass Hintergrundgeräusche aufgenommen werden
- Das Mikrofon startet standardmäßig **gemutet** - drücke **M** zum Aktivieren
- Claude.ai erlaubt max. **100 Bilder pro Chat**. Bei 90/95 Bildern warnt die Bridge
- 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!)
---

View File

@ -94,9 +94,11 @@ class ClaudesEyesAudioBridge:
"""
def __init__(self, config_path: str):
self.config_path = Path(config_path)
self.config = self._load_config(config_path)
self.running = False
self.stats = BridgeStats()
self._previous_chat_id: Optional[str] = None # Für Referenz zum alten Chat
# Komponenten (werden in initialize() erstellt)
self.chat: Optional[ClaudeChatInterface] = None
@ -119,7 +121,8 @@ class ClaudesEyesAudioBridge:
self._sending.set() # Anfangs nicht am Senden (set = frei)
# Mute-Flag: Wenn True, ignoriert STT alle Eingaben
self._muted = False
# Startet gemutet um ungewollte Aufnahmen zu vermeiden
self._muted = True
self._mute_lock = threading.Lock()
def _load_config(self, config_path: str) -> dict:
@ -132,6 +135,9 @@ class ClaudesEyesAudioBridge:
path = local_path
logger.info(f"Nutze lokale Config: {path}")
# Merke den tatsächlichen Pfad für späteres Speichern
self._actual_config_path = path
if not path.exists():
logger.error(f"Config nicht gefunden: {path}")
sys.exit(1)
@ -139,6 +145,48 @@ class ClaudesEyesAudioBridge:
with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def _save_chat_url_to_config(self, new_url: str):
"""
Speichert die neue Chat-URL in der config.yaml.
Aktualisiert nur den chat.url Wert, behält Rest der Datei bei.
"""
try:
config_path = getattr(self, '_actual_config_path', self.config_path)
# Config-Datei lesen und als Text bearbeiten (um Kommentare zu erhalten)
with open(config_path, 'r', encoding='utf-8') as f:
content = f.read()
# URL im Text ersetzen (regex für chat.url Zeile)
import re
old_url = self.config.get("chat", {}).get("url", "")
if old_url:
# Ersetze die alte URL mit der neuen
content = content.replace(old_url, new_url)
else:
# Fallback: Ersetze die Zeile mit url:
content = re.sub(
r'(url:\s*["\'])([^"\']+)(["\'])',
rf'\g<1>{new_url}\g<3>',
content
)
# Speichern
with open(config_path, 'w', encoding='utf-8') as f:
f.write(content)
# Config im Speicher auch aktualisieren
self.config["chat"]["url"] = new_url
logger.info(f"Neue Chat-URL in Config gespeichert: {new_url}")
console.print(f"[green]✓ Config aktualisiert: {config_path}[/green]")
except Exception as e:
logger.error(f"Fehler beim Speichern der Chat-URL: {e}")
console.print(f"[yellow]⚠ Konnte Config nicht aktualisieren: {e}[/yellow]")
def initialize(self) -> bool:
"""Initialisiert alle Komponenten"""
@ -266,7 +314,8 @@ class ClaudesEyesAudioBridge:
threads.append(t4)
console.print("[cyan]Bridge läuft![/cyan]")
console.print("[dim]Drücke 'M' für Mute/Unmute, Ctrl+C zum Beenden[/dim]\n")
console.print("[bold red]🎤 Mikrofon ist GEMUTET[/bold red] - Drücke 'M' zum Aktivieren")
console.print("[dim]Tasten: M=Mute/Unmute, N=Neuer Chat, Q=Beenden[/dim]\n")
# Sende Startsignal an Claude und warte auf [READY]
if not self._send_start_signal():
@ -314,17 +363,30 @@ class ClaudesEyesAudioBridge:
console.print("\n[green]Bridge beendet.[/green]")
def _send_start_signal(self) -> bool:
def _send_start_signal(self, previous_chat_id: Optional[str] = None) -> bool:
"""
Sendet das Startsignal und die Instruktionen an Claude.
Args:
previous_chat_id: Optional - Chat-ID des vorherigen Chats (für Kontinuität)
Returns:
True wenn Claude mit [READY] antwortet
"""
# Instruktionen für Claude
intro_message = """[START] Hallo Claude! Du steuerst jetzt einen echten Roboter - "Claude's Eyes"!
# Kontext vom vorherigen Chat, falls vorhanden
previous_chat_context = ""
if previous_chat_id:
previous_chat_context = f"""
## WICHTIG: Fortsetzung eines vorherigen Chats!
Dies ist eine Fortsetzung unserer Erkundung. Der vorherige Chat hatte die ID: {previous_chat_id}
Falls du dich an den Kontext erinnerst, kannst du dort weitermachen wo wir aufgehört haben.
Der Chat-Wechsel war nötig weil Claude.ai ein Limit von 100 Bildern pro Chat hat.
## Deine Fähigkeiten
"""
# Instruktionen für Claude
intro_message = f"""[START] Hallo Claude! Du steuerst jetzt einen echten Roboter - "Claude's Eyes"!
{previous_chat_context}## Deine Fähigkeiten
Du hast Zugriff auf einen ESP32-Roboter mit:
- **Kamera** (OV5640, 120° Weitwinkel) - deine Augen
- **4 Motoren** - deine Beine
@ -458,12 +520,26 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
try:
# Erst Bild hochladen wenn aktiviert
image_uploaded = False
if upload_images:
# Bild holen und hochladen
# Bild holen
if not self.chat.fetch_image_from_esp32():
logger.warning("Konnte kein Bild vom ESP32 holen")
elif not self.chat.upload_image_to_chat():
logger.warning("Konnte Bild nicht hochladen")
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:
@ -589,17 +665,18 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
def _keyboard_loop(self):
"""
Hört auf Tastatureingaben für Mute-Toggle.
Hört auf Tastatureingaben für Mute-Toggle und andere Befehle.
Tasten:
- M: Mute/Unmute Toggle
- N: Neuer Chat starten (bei Bilder-Limit)
- Q: Beenden (alternativ zu Ctrl+C)
"""
import sys
import tty
import termios
logger.info("Keyboard-Loop gestartet (M=Mute, Q=Quit)")
logger.info("Keyboard-Loop gestartet (M=Mute, N=New Chat, Q=Quit)")
# Speichere ursprüngliche Terminal-Settings
old_settings = None
@ -621,6 +698,8 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
if char.lower() == 'm':
self.toggle_mute()
elif char.lower() == 'n':
self._start_new_chat_with_instructions()
elif char.lower() == 'q' or char == '\x03': # q oder Ctrl+C
console.print("\n[yellow]Beende Bridge...[/yellow]")
self.running = False
@ -633,6 +712,36 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
if old_settings:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
def _start_new_chat_with_instructions(self):
"""Startet einen neuen Chat und sendet die Instruktionen erneut"""
console.print("\n[yellow]Starte neuen Chat...[/yellow]")
# Heartbeat pausieren
self._claude_ready.clear()
with self._lock:
new_url, old_chat_id = self.chat.start_new_chat()
if new_url:
console.print(f"[green]Neuer Chat: {new_url}[/green]")
if old_chat_id:
console.print(f"[dim]Vorheriger Chat: {old_chat_id}[/dim]")
# Neue URL in config.yaml speichern
self._save_chat_url_to_config(new_url)
console.print("[cyan]Sende Instruktionen mit Referenz zum alten Chat...[/cyan]")
# Instruktionen erneut senden (mit Referenz zum alten Chat)
if self._send_start_signal(previous_chat_id=old_chat_id):
console.print("[bold green]Claude ist bereit! Heartbeat läuft weiter.[/bold green]\n")
else:
console.print("[red]Claude hat nicht mit [READY] geantwortet.[/red]")
else:
console.print("[red]Konnte keinen neuen Chat starten![/red]")
# Heartbeat wieder aktivieren
self._claude_ready.set()
def _get_and_clear_stefan_buffer(self) -> Optional[str]:
"""
Holt den Stefan-Buffer und leert ihn.

View File

@ -11,6 +11,7 @@ wenn Claude.ai sein UI ändert.
import time
import logging
import tempfile
import hashlib
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
@ -50,6 +51,26 @@ class ClaudeChatInterface:
WICHTIG: Du musst beim ersten Start manuell einloggen!
"""
@staticmethod
def extract_chat_id(url: str) -> Optional[str]:
"""
Extrahiert die Chat-ID aus einer Claude.ai URL.
Args:
url: z.B. "https://claude.ai/chat/21ac7549-1009-44cc-a143-3e4bd3c64b2d"
Returns:
Chat-ID z.B. "21ac7549-1009-44cc-a143-3e4bd3c64b2d", oder None
"""
if not url or '/chat/' not in url:
return None
try:
# Extrahiere alles nach /chat/
chat_id = url.split('/chat/')[-1].split('?')[0].split('#')[0]
return chat_id if chat_id else None
except:
return None
# CSS Selektoren für Claude.ai (Stand: Dezember 2025)
# Diese müssen angepasst werden wenn sich das UI ändert!
SELECTORS = {
@ -106,6 +127,10 @@ class ClaudeChatInterface:
self._last_message_id = 0
self._temp_image_path = Path(tempfile.gettempdir()) / "robot_view.jpg"
# Für Bild-Deduplizierung (100 Bilder pro Chat Limit!)
self._last_image_hash: Optional[str] = None
self._images_uploaded = 0
# HTTP Session mit größerem Connection Pool (vermeidet "pool full" Warnungen)
self._http_session = requests.Session()
adapter = HTTPAdapter(pool_connections=10, pool_maxsize=10)
@ -162,6 +187,10 @@ class ClaudeChatInterface:
self.driver.get(url)
self.chat_url = url
# Reset Bild-Counter bei neuem Chat
self._last_image_hash = None
self._images_uploaded = 0
# Warte auf Seitenladung
time.sleep(3)
@ -180,6 +209,76 @@ class ClaudeChatInterface:
logger.info("Login erfolgreich!")
time.sleep(2)
def start_new_chat(self) -> tuple[Optional[str], Optional[str]]:
"""
Startet einen neuen Chat auf Claude.ai.
WICHTIG: Nutze dies wenn das 100-Bilder-Limit erreicht ist!
Returns:
Tuple (neue_url, alte_chat_id) oder (None, None) bei Fehler
"""
try:
# Alte Chat-ID merken bevor wir wechseln
old_chat_id = self.extract_chat_id(self.chat_url)
logger.info(f"Starte neuen Chat... (alter Chat: {old_chat_id})")
# Methode 1: "New Chat" Button suchen
new_chat_selectors = [
"button[aria-label*='New']",
"button[aria-label*='new']",
"a[href='/new']",
"a[href*='/chat/new']",
"[data-testid*='new-chat']",
"button:has-text('New chat')",
]
for selector in new_chat_selectors:
try:
elements = self.driver.find_elements(By.CSS_SELECTOR, selector)
for elem in elements:
if elem.is_displayed():
elem.click()
time.sleep(2)
new_url = self.driver.current_url
logger.info(f"Neuer Chat gestartet: {new_url}")
# Reset Counter
self._last_image_hash = None
self._images_uploaded = 0
self.chat_url = new_url
return new_url, old_chat_id
except:
continue
# Methode 2: Direkt zu /new navigieren
base_url = self.chat_url.split('/chat/')[0] if '/chat/' in self.chat_url else "https://claude.ai"
new_url = f"{base_url}/new"
logger.info(f"Versuche direkte Navigation zu: {new_url}")
self.driver.get(new_url)
time.sleep(3)
# Prüfe ob es geklappt hat
current = self.driver.current_url
if current != self.chat_url:
logger.info(f"Neuer Chat: {current}")
self._last_image_hash = None
self._images_uploaded = 0
self.chat_url = current
return current, old_chat_id
logger.warning("Konnte keinen neuen Chat starten")
return None, None
except Exception as e:
logger.error(f"Fehler beim Starten eines neuen Chats: {e}")
return None, None
def get_current_chat_url(self) -> str:
"""Gibt die aktuelle Chat-URL zurück"""
return self.driver.current_url
def send_message(self, text: str, wait_for_response: bool = False) -> bool:
"""
Sendet eine Nachricht in den Chat.
@ -680,6 +779,7 @@ class ClaudeChatInterface:
file_input.send_keys(str(self._temp_image_path.absolute()))
logger.info("Bild hochgeladen!")
self._images_uploaded += 1
# Kurz warten bis Upload verarbeitet ist
time.sleep(1.5)
@ -690,6 +790,131 @@ class ClaudeChatInterface:
logger.error(f"Fehler beim Bild-Upload: {e}")
return False
def _calculate_image_hash(self) -> Optional[str]:
"""
Berechnet einen Hash des aktuellen Bildes.
Returns:
MD5 Hash als Hex-String, oder None bei Fehler
"""
if not self._temp_image_path.exists():
return None
try:
with open(self._temp_image_path, "rb") as f:
return hashlib.md5(f.read()).hexdigest()
except Exception as e:
logger.debug(f"Hash-Berechnung fehlgeschlagen: {e}")
return None
def has_image_changed(self) -> bool:
"""
Prüft ob sich das Bild seit dem letzten Upload geändert hat.
WICHTIG: Claude.ai hat ein Limit von 100 Dateien pro Chat!
Diese Funktion hilft, unnötige Uploads zu vermeiden.
Returns:
True wenn Bild neu/geändert, False wenn identisch zum letzten
"""
current_hash = self._calculate_image_hash()
if current_hash is None:
return True # Kein Bild = als "geändert" behandeln
if self._last_image_hash is None:
return True # Erster Upload
return current_hash != self._last_image_hash
def upload_image_if_changed(self) -> bool:
"""
Lädt Bild nur hoch wenn es sich geändert hat.
Returns:
True wenn hochgeladen, False wenn übersprungen oder Fehler
"""
if not self.has_image_changed():
logger.debug("Bild unverändert - überspringe Upload")
return False
# Hash vor dem Upload speichern
new_hash = self._calculate_image_hash()
if self.upload_image_to_chat():
self._last_image_hash = new_hash
return True
return False
def delete_uploaded_images(self) -> int:
"""
Versucht hochgeladene Bilder aus dem Chat-Eingabefeld zu löschen.
HINWEIS: Funktioniert nur für Bilder die noch nicht gesendet wurden!
Bereits gesendete Bilder können nicht gelöscht werden.
Returns:
Anzahl gelöschter Bilder
"""
deleted = 0
try:
# Suche nach Bild-Vorschau-Elementen im Eingabebereich
# Diese haben meist einen X/Close Button
preview_selectors = [
"button[aria-label*='Remove']",
"button[aria-label*='remove']",
"button[aria-label*='Delete']",
"button[aria-label*='delete']",
"[class*='preview'] button[class*='close']",
"[class*='attachment'] button[class*='remove']",
"[class*='file'] button[class*='delete']",
]
for selector in preview_selectors:
try:
buttons = self.driver.find_elements(By.CSS_SELECTOR, selector)
for btn in buttons:
if btn.is_displayed():
btn.click()
deleted += 1
time.sleep(0.3)
except:
continue
# JavaScript-Fallback
if deleted == 0:
try:
deleted = self.driver.execute_script("""
let count = 0;
// Suche nach Remove-Buttons in Attachment-Previews
const buttons = document.querySelectorAll(
'[class*="preview"] button, [class*="attachment"] button'
);
buttons.forEach(btn => {
if (btn.offsetParent) {
btn.click();
count++;
}
});
return count;
""") or 0
except:
pass
if deleted > 0:
logger.info(f"{deleted} Bild(er) aus Eingabefeld gelöscht")
except Exception as e:
logger.debug(f"Bild-Löschung fehlgeschlagen: {e}")
return deleted
def get_images_uploaded_count(self) -> int:
"""Gibt Anzahl der in dieser Session hochgeladenen Bilder zurück"""
return self._images_uploaded
def _find_file_input(self):
"""Findet das File-Upload Input Element"""
selectors = [