fix(voice_loop): PipeWire-Aufnahme (pw-record) als Standard via --recorder auto

- 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 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 10:37:49 +02:00
commit d15e51eb0d
2 changed files with 56 additions and 11 deletions

View file

@ -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`.

View file

@ -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)