From 4254475b73fe6b1522ea278dbe3843c55d9c98da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Wed, 17 Jun 2026 11:00:13 +0200 Subject: [PATCH] fix(voice_loop): Keepalive-Timeout (1011) bei langer Aufnahme/Wiedergabe behoben Eine Dauerverbindung wurde vom Server geschlossen, wenn Aufnahme/Wiedergabe den Event-Loop blockierten (Keepalive-Ping unbeantwortet) - sichtbar bei langen TTS-Antworten. Jetzt: kurze WebSocket-Verbindung PRO Sprech-Runde; das Audio wird erst NACH dem Schliessen abgespielt. Gespraechsgedaechtnis bleibt serverseitig via session_id erhalten. Verifiziert mit langer Antwort (~12 s Audio) ohne Abbruch. Co-Authored-By: Claude Opus 4.8 --- scripts/voice_loop.py | 69 ++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/scripts/voice_loop.py b/scripts/voice_loop.py index 3822552..e3e6094 100644 --- a/scripts/voice_loop.py +++ b/scripts/voice_loop.py @@ -131,7 +131,9 @@ def _start_frame(args) -> dict: return frame -async def one_turn(ws, wav: bytes, start_frame: dict) -> None: +async def one_turn(ws, wav: bytes, start_frame: dict) -> bytes: + """Sendet eine Aeusserung, gibt das Antwort-Audio (PCM) zurueck. Spielt NICHT ab, + damit die Wiedergabe ausserhalb der offenen Verbindung passieren kann.""" await ws.send(json.dumps(start_frame)) await ws.send(wav) await ws.send(json.dumps({"type": "end"})) @@ -152,9 +154,19 @@ async def one_turn(ws, wav: bytes, start_frame: dict) -> None: print(f" ⚠ NOTFALL erkannt (Kategorie: {event.get('category')})") elif etype == "error": print(f" Fehler {event.get('status','')}: {event.get('detail')}") - return + return b"" elif etype == "done": break + return audio + + +async def _send_and_play(url: str, wav: bytes, start_frame: dict) -> None: + """Oeffnet eine kurze Verbindung pro Runde (Gedaechtnis bleibt serverseitig via + session_id erhalten), empfaengt die Antwort und spielt sie NACH dem Schliessen ab. + So blockiert die (lange) Aufnahme/Wiedergabe nie eine offene WebSocket-Verbindung + (kein Keepalive-Timeout).""" + async with websockets.connect(url, max_size=None) as ws: + audio = await one_turn(ws, wav, start_frame) if audio: play_pcm(audio) @@ -165,32 +177,35 @@ async def run(args) -> None: url += f"&token={args.token}" start_frame = _start_frame(args) - async with websockets.connect(url, max_size=None) as ws: - print(f"Verbunden: {args.url} (Session '{args.session}')") - if args.file: - with open(args.file, "rb") as fh: - wav = fh.read() - print(f"Sende Datei: {args.file}") - await one_turn(ws, wav, start_frame) + if args.file: + with open(args.file, "rb") as fh: + wav = fh.read() + print(f"Sende Datei: {args.file}") + await _send_and_play(url, wav, start_frame) + return + + recorder = resolve_recorder(args.recorder) + print(f"Ziel: {args.url} (Session '{args.session}')") + print(f"Aufnahme mit: {recorder}" + + (f" (Gerät: {args.device})" if args.device else " (Standardgerät)")) + print("Sprich nach 'START', stoppe mit Enter. Strg+C beendet den Loop.") + while True: + try: + wav = record_utterance(recorder, args.device, args.rate) + except KeyboardInterrupt: + print("\nEnde.") return - recorder = resolve_recorder(args.recorder) - print(f"Aufnahme mit: {recorder}" - + (f" (Gerät: {args.device})" if args.device else " (Standardgerät)")) - print("Sprich nach 'START', stoppe mit Enter. Strg+C beendet den Loop.") - while True: - try: - wav = record_utterance(recorder, args.device, args.rate) - except KeyboardInterrupt: - print("\nEnde.") - return - if len(wav) < 1000: - print(" ⚠ Keine/zu kurze Aufnahme. Moegliche Ursache: Aufnahmewerkzeug" - " oder Geraet nicht nutzbar.") - print(" Verfuegbare Mikrofone: arecord -l") - print(" Direktes ALSA-Geraet verwenden, z. B.:") - print(" python scripts/voice_loop.py --recorder arecord --device plughw:2,0") - continue - await one_turn(ws, wav, start_frame) + if len(wav) < 1000: + print(" ⚠ Keine/zu kurze Aufnahme. Moegliche Ursache: Aufnahmewerkzeug" + " oder Geraet nicht nutzbar.") + print(" Verfuegbare Mikrofone: arecord -l") + print(" Direktes ALSA-Geraet verwenden, z. B.:") + print(" python scripts/voice_loop.py --recorder arecord --device plughw:2,0") + continue + try: + await _send_and_play(url, wav, start_frame) + except Exception as exc: # noqa: BLE001 - Verbindung pro Runde; Fehler nicht fatal + print(f" Verbindungsfehler: {exc}") def main() -> None: