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

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