neuer chat auf grund 100 dateien in chat implementiert und check ob bilder doppelt
This commit is contained in:
parent
095385a4a5
commit
90791b9ff6
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue