mute funtion und warten bis nachricht gesendet

This commit is contained in:
duffyduck 2025-12-27 02:20:41 +01:00
parent 28d2bfe7d1
commit 095385a4a5
3 changed files with 146 additions and 23 deletions

View File

@ -167,10 +167,19 @@ Claude verwendet diese Befehle in eckigen Klammern:
- **Paralelle Konversation** - Erkunden UND quatschen gleichzeitig - **Paralelle Konversation** - Erkunden UND quatschen gleichzeitig
- **Sprachausgabe** - Claude redet mit dir (TTS) - **Sprachausgabe** - Claude redet mit dir (TTS)
- **Spracheingabe** - Du redest mit Claude (STT) - **Spracheingabe** - Du redest mit Claude (STT)
- **Mute/Unmute** - Mikrofon per Tastendruck stummschalten
- **Hinderniserkennung** - Ultraschall & IMU - **Hinderniserkennung** - Ultraschall & IMU
- **Touch-Display** - Notfall-Stopp & Status - **Touch-Display** - Notfall-Stopp & Status
- **Termux Support** - Läuft auch auf Android! - **Termux Support** - Läuft auch auf Android!
## Keyboard-Shortcuts (Bridge)
| Taste | Funktion |
|-------|----------|
| **M** | Mikrofon Mute/Unmute |
| **Q** | Bridge beenden |
| **Ctrl+C** | Bridge beenden |
--- ---
## Sicherheit ## Sicherheit

View File

@ -229,6 +229,16 @@ python chat_audio_bridge.py -d
python chat_audio_bridge.py -c config.local.yaml python chat_audio_bridge.py -c config.local.yaml
``` ```
### 2.6 Keyboard-Shortcuts während der Bridge läuft
| Taste | Funktion |
|-------|----------|
| **M** | Mikrofon Mute/Unmute - schaltet STT stumm |
| **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!
--- ---
## Teil 3: Hardware zusammenbauen ## Teil 3: Hardware zusammenbauen

View File

@ -114,6 +114,14 @@ class ClaudesEyesAudioBridge:
self._stefan_buffer: list = [] self._stefan_buffer: list = []
self._stefan_buffer_lock = threading.Lock() self._stefan_buffer_lock = threading.Lock()
# Send-Lock: Verhindert dass TICKs während Senden reinkommen
self._sending = threading.Event()
self._sending.set() # Anfangs nicht am Senden (set = frei)
# Mute-Flag: Wenn True, ignoriert STT alle Eingaben
self._muted = False
self._mute_lock = threading.Lock()
def _load_config(self, config_path: str) -> dict: def _load_config(self, config_path: str) -> dict:
"""Lädt die Konfiguration""" """Lädt die Konfiguration"""
path = Path(config_path) path = Path(config_path)
@ -252,8 +260,13 @@ class ClaudesEyesAudioBridge:
t3.start() t3.start()
threads.append(t3) threads.append(t3)
# Thread 4: Keyboard-Listener für Mute-Toggle
t4 = threading.Thread(target=self._keyboard_loop, name="Keyboard", daemon=True)
t4.start()
threads.append(t4)
console.print("[cyan]Bridge läuft![/cyan]") console.print("[cyan]Bridge läuft![/cyan]")
console.print("[dim]Drücke Ctrl+C zum Beenden[/dim]\n") console.print("[dim]Drücke 'M' für Mute/Unmute, Ctrl+C zum Beenden[/dim]\n")
# Sende Startsignal an Claude und warte auf [READY] # Sende Startsignal an Claude und warte auf [READY]
if not self._send_start_signal(): if not self._send_start_signal():
@ -435,33 +448,43 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
# Stefan-Buffer holen (falls er was gesagt hat) # Stefan-Buffer holen (falls er was gesagt hat)
stefan_text = self._get_and_clear_stefan_buffer() stefan_text = self._get_and_clear_stefan_buffer()
# Warte bis vorheriges Senden fertig ist
self._sending.wait()
# Nächsten TICK senden (mit oder ohne Bild) # Nächsten TICK senden (mit oder ohne Bild)
with self._lock: with self._lock:
# Erst Bild hochladen wenn aktiviert # Signalisiere dass wir senden
if upload_images: self._sending.clear()
# Bild holen und hochladen
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")
# Nachricht zusammenbauen try:
if stefan_text: # Erst Bild hochladen wenn aktiviert
# Stefan hat was gesagt → Mit TICK senden if upload_images:
tick_message = f"[TICK]\n\nStefan sagt: {stefan_text}" # Bild holen und hochladen
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]") if not self.chat.fetch_image_from_esp32():
else: logger.warning("Konnte kein Bild vom ESP32 holen")
# Nur TICK elif not self.chat.upload_image_to_chat():
tick_message = "[TICK]" logger.warning("Konnte Bild nicht hochladen")
success = self.chat.send_message(tick_message) # 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_message = "[TICK]"
if success: success = self.chat.send_message(tick_message)
self.stats.ticks_sent += 1
self.stats.consecutive_errors = 0 # Reset if success:
logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if upload_images else "") + (f" + Stefan: {stefan_text[:30]}" if stefan_text else "")) self.stats.ticks_sent += 1
else: self.stats.consecutive_errors = 0 # Reset
raise Exception("TICK fehlgeschlagen") logger.debug(f"TICK #{self.stats.ticks_sent}" + (" mit Bild" if upload_images else "") + (f" + Stefan: {stefan_text[:30]}" if stefan_text else ""))
else:
raise Exception("TICK fehlgeschlagen")
finally:
# Senden fertig - wieder freigeben
self._sending.set()
except Exception as e: except Exception as e:
logger.error(f"Heartbeat-Fehler: {e}") logger.error(f"Heartbeat-Fehler: {e}")
@ -525,6 +548,7 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
Wenn Claude tippt Buffer sammeln Wenn Claude tippt Buffer sammeln
Wenn Claude fertig Buffer wird mit nächstem TICK gesendet Wenn Claude fertig Buffer wird mit nächstem TICK gesendet
Wenn gemutet Ignoriert alle Eingaben
So wird Claude nicht unterbrochen und bekommt alles gesammelt. So wird Claude nicht unterbrochen und bekommt alles gesammelt.
""" """
@ -536,9 +560,18 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
while self.running: while self.running:
try: try:
# Wenn gemutet, kurz warten und überspringen
if self.is_muted():
time.sleep(0.5)
continue
# Warte auf Sprache (mit Timeout) # Warte auf Sprache (mit Timeout)
result = self.stt.listen_once(timeout=2) result = self.stt.listen_once(timeout=2)
# Nochmal prüfen nach dem Hören (falls zwischendurch gemutet wurde)
if self.is_muted():
continue
if result and result.text and len(result.text) > 2: if result and result.text and len(result.text) > 2:
# In Buffer speichern (thread-safe) # In Buffer speichern (thread-safe)
with self._stefan_buffer_lock: with self._stefan_buffer_lock:
@ -554,6 +587,52 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
logger.error(f"STT-Loop-Fehler: {e}") logger.error(f"STT-Loop-Fehler: {e}")
self.stats.errors += 1 self.stats.errors += 1
def _keyboard_loop(self):
"""
Hört auf Tastatureingaben für Mute-Toggle.
Tasten:
- M: Mute/Unmute Toggle
- Q: Beenden (alternativ zu Ctrl+C)
"""
import sys
import tty
import termios
logger.info("Keyboard-Loop gestartet (M=Mute, Q=Quit)")
# Speichere ursprüngliche Terminal-Settings
old_settings = None
try:
old_settings = termios.tcgetattr(sys.stdin)
except:
logger.warning("Konnte Terminal-Settings nicht lesen (kein TTY?)")
return
try:
# Terminal in raw mode setzen (einzelne Tasten ohne Enter)
tty.setraw(sys.stdin.fileno())
while self.running:
# Lese einzelnes Zeichen (blockierend, aber mit select für Timeout)
import select
if select.select([sys.stdin], [], [], 0.5)[0]:
char = sys.stdin.read(1)
if char.lower() == 'm':
self.toggle_mute()
elif char.lower() == 'q' or char == '\x03': # q oder Ctrl+C
console.print("\n[yellow]Beende Bridge...[/yellow]")
self.running = False
break
except Exception as e:
logger.debug(f"Keyboard-Loop Fehler: {e}")
finally:
# Terminal-Settings wiederherstellen
if old_settings:
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
def _get_and_clear_stefan_buffer(self) -> Optional[str]: def _get_and_clear_stefan_buffer(self) -> Optional[str]:
""" """
Holt den Stefan-Buffer und leert ihn. Holt den Stefan-Buffer und leert ihn.
@ -571,6 +650,31 @@ Erst dann starten die automatischen TICKs mit Bildern!"""
return text return text
def toggle_mute(self) -> bool:
"""
Schaltet Mute um.
Returns:
True wenn jetzt gemutet, False wenn ungemutet
"""
with self._mute_lock:
self._muted = not self._muted
status = "MUTED" if self._muted else "UNMUTED"
console.print(f"\n[bold {'red' if self._muted else 'green'}]🎤 Mikrofon {status}[/bold {'red' if self._muted else 'green'}]")
return self._muted
def set_mute(self, muted: bool):
"""Setzt Mute-Status direkt"""
with self._mute_lock:
self._muted = muted
status = "MUTED" if self._muted else "UNMUTED"
console.print(f"\n[bold {'red' if self._muted else 'green'}]🎤 Mikrofon {status}[/bold {'red' if self._muted else 'green'}]")
def is_muted(self) -> bool:
"""Prüft ob gemutet"""
with self._mute_lock:
return self._muted
def _clean_for_speech(self, text: str) -> str: def _clean_for_speech(self, text: str) -> str:
""" """
Entfernt Befehle und technische Teile aus dem Text. Entfernt Befehle und technische Teile aus dem Text.