mute funtion und warten bis nachricht gesendet
This commit is contained in:
parent
28d2bfe7d1
commit
095385a4a5
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue