From d15e51eb0d7c7e56ae958c65140076b45319dde6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Wed, 17 Jun 2026 10:37:49 +0200 Subject: [PATCH] fix(voice_loop): PipeWire-Aufnahme (pw-record) als Standard via --recorder auto MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - arecord scheiterte auf PipeWire-Systemen ("Fehler beim Öffnen des Gerätes") - neue Option --recorder {auto,pw-record,parecord,arecord}; auto bevorzugt pw-record - geraetespezifische Flags je Werkzeug (-D / --target / --device) - Doku: Recorder-Hinweis + Troubleshooting-Eintrag fuer den arecord-Fehler - verifiziert: auto -> pw-record; --file-Round-Trip weiterhin ok Co-Authored-By: Claude Opus 4.8 --- BEDIENUNGSANLEITUNG.md | 11 +++++++-- scripts/voice_loop.py | 56 +++++++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index d363595..082a921 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -106,12 +106,18 @@ Ablauf je Runde: Nützliche Optionen: ```bash -python scripts/voice_loop.py --device hw:1,0 # bestimmtes Mikrofon (siehe: arecord -L) +python scripts/voice_loop.py --recorder pw-record # PipeWire-Aufnahme (Standard bei 'auto') +python scripts/voice_loop.py --recorder arecord --device hw:1,0 # ALSA, bestimmtes Mikrofon python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter python scripts/voice_loop.py --token "$TOKEN" # falls AUTH_ENABLED=true python scripts/voice_loop.py --file frage.wav # ohne Mikrofon: WAV senden (Test) ``` +> **Aufnahmewerkzeug:** `--recorder auto` (Standard) bevorzugt **PipeWire** (`pw-record`), +> sonst `parecord`/`arecord`. Gibt `arecord` den Fehler *„Fehler beim Öffnen des +> Gerätes"* aus, nutze `--recorder pw-record` (auf PipeWire-Systemen der Normalfall) +> oder gib mit `--device` ein konkretes Gerät an. + ## A2. Nur tippen → Antwort hören ```bash @@ -371,7 +377,8 @@ curl -s "$URL/api/metrics?format=prometheus" # Prometheus-Text (kein jq) | HTTP **502** bei STT/TTS | Cloud-Fehler/Format | `make smoke` ausführen; Modellnamen in `.env` prüfen | | `VA_PROFILE` wirkt nicht | `DEFAULT_*_PROVIDER` in `.env` überschreibt | diese Zeilen in `.env` auskommentieren | | `Address already in use` | Port belegt | anderen `PORT` setzen | -| Keine Aufnahme/Wiedergabe | `arecord`/Player/Gerät | `arecord -L` / `aplay -L`; Paket `alsa-utils`, `ffmpeg` | +| `arecord: Fehler beim Öffnen des Gerätes` | ALSA-`default` auf PipeWire nicht nutzbar | `--recorder pw-record` (Standard bei `auto`) oder `--device` mit Gerät aus `arecord -L` | +| Keine Aufnahme/Wiedergabe | Werkzeug/Gerät fehlt | `arecord -L` / `aplay -L`; Pakete `pipewire`/`alsa-utils`/`ffmpeg`; Default via `wpctl status` | | Profil greift nicht | `config/voice-assistant.toml` fehlt | aus `*.example.toml` kopieren (Abschnitt 2) | Logs erscheinen im Terminal von `make run`; mehr Details mit `LOG_LEVEL=debug` in `.env`. diff --git a/scripts/voice_loop.py b/scripts/voice_loop.py index ffcc40c..43dc4e7 100644 --- a/scripts/voice_loop.py +++ b/scripts/voice_loop.py @@ -54,21 +54,52 @@ def _first_player() -> list[str] | None: return None -def record_utterance(device: str | None, rate: int) -> bytes: +def resolve_recorder(choice: str) -> str: + """Waehlt das Aufnahmewerkzeug. 'auto' bevorzugt PipeWire (pw-record).""" + if choice != "auto": + if not shutil.which(choice): + sys.exit(f"Fehlt: Aufnahmewerkzeug '{choice}' nicht gefunden.") + return choice + for tool in ("pw-record", "parecord", "arecord"): + if shutil.which(tool): + return tool + sys.exit("Kein Aufnahmewerkzeug gefunden (pw-record / parecord / arecord).") + + +def _record_cmd(recorder: str, device: str | None, rate: int, outfile: str) -> list[str]: + if recorder == "arecord": + cmd = ["arecord", "-q", "-f", "S16_LE", "-r", str(rate), "-c", "1"] + if device: + cmd += ["-D", device] + return cmd + [outfile] + if recorder == "pw-record": + cmd = ["pw-record", "--rate", str(rate), "--channels", "1", "--format", "s16"] + if device: + cmd += ["--target", device] + return cmd + [outfile] + if recorder == "parecord": + cmd = ["parecord", f"--rate={rate}", "--channels=1", "--format=s16le", + "--file-format=wav"] + if device: + cmd += [f"--device={device}"] + return cmd + [outfile] + sys.exit(f"Unbekanntes Aufnahmewerkzeug: {recorder}") + + +def record_utterance(recorder: str, device: str | None, rate: int) -> bytes: """Push-to-Talk: Enter startet, Enter stoppt die Aufnahme; gibt WAV-Bytes zurueck.""" - _require("arecord") tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) tmp.close() - cmd = ["arecord", "-q", "-f", "S16_LE", "-r", str(rate), "-c", "1"] - if device: - cmd += ["-D", device] - cmd.append(tmp.name) + cmd = _record_cmd(recorder, device, rate, tmp.name) input("\n[Enter] = Aufnahme START …") proc = subprocess.Popen(cmd) input("[Enter] = Aufnahme STOP …") proc.send_signal(signal.SIGINT) - proc.wait(timeout=5) + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() with open(tmp.name, "rb") as fh: return fh.read() @@ -142,10 +173,13 @@ async def run(args) -> None: print(f"Sende Datei: {args.file}") await one_turn(ws, wav, start_frame) 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(args.device, args.rate) + wav = record_utterance(recorder, args.device, args.rate) except KeyboardInterrupt: print("\nEnde.") return @@ -160,7 +194,11 @@ def main() -> None: p.add_argument("--url", default="ws://127.0.0.1:8003/ws/voice") p.add_argument("--session", default="voice-loop") p.add_argument("--token", default=None, help="Bearer-Token, falls AUTH_ENABLED=true") - p.add_argument("--device", default=None, help="Aufnahmegeraet (siehe: arecord -L)") + p.add_argument("--device", default=None, + help="Aufnahmegeraet (arecord: -L; pw-record: --target; parecord: --device)") + p.add_argument("--recorder", default="auto", + choices=["auto", "pw-record", "parecord", "arecord"], + help="Aufnahmewerkzeug; 'auto' bevorzugt PipeWire (pw-record)") p.add_argument("--rate", type=int, default=16000) p.add_argument("--stt-provider", dest="stt_provider", default=None) p.add_argument("--llm-provider", dest="llm_provider", default=None)