diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index dec938f..f1c9b4a 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -111,22 +111,21 @@ Nützliche Optionen: ```bash python scripts/voice_loop.py --stream-text # Antworttext live anzeigen, waehrend die KI generiert python scripts/voice_loop.py --no-stream-audio # satzweises Vorlesen abschalten (Audio erst komplett) -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 --recorder arecord --device plughw:6,0 # bestimmtes Mikrofon erzwingen 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`. **Falls die Aufnahme scheitert** (z. B. -> *„pw_context_connect() failed"* oder *„Fehler beim Öffnen des Gerätes"*), ist der -> verlässlichste Weg ein **direktes ALSA-Hardware-Gerät**: -> ```bash -> arecord -l # Kartennummern der Mikrofone -> python scripts/voice_loop.py --recorder arecord --device plughw:2,0 -> ``` -> (`plughw:KARTE,GERÄT` aus `arecord -l`; z. B. onboard oft Karte 2, USB-Webcam Karte 4.) +> **Geräte = System-Standard (automatisch):** Ohne `--device` folgt der Loop dem +> **am System eingestellten Standard-Mikrofon und -Lautsprecher** (inkl. Bluetooth — +> umstellbar über *Ubuntu → Einstellungen → Ton*, siehe Teil B2). `--recorder auto` +> (Standard) wählt selbsttätig ein Aufnahmewerkzeug, das dem Default folgt **und** im +> **Kurztest wirklich Audio liefert** (Reihenfolge `ffmpeg` → `parecord` → `arecord` → +> `pw-record`) — so wird nie ein totes Gerät gewählt. Beim Start erscheint kurz +> „Prüfe Standard-Aufnahmegerät …". +> **Ein bestimmtes Mikrofon** nur bei Bedarf erzwingen, z. B. `--recorder arecord +> --device plughw:6,0` (`arecord -l` zeigt die Kartennummer). ## A2. Nur tippen → Antwort hören @@ -390,20 +389,24 @@ pactl set-default-sink # z. B. die Bluetooth-Box Sobald die Bluetooth-Box dort als Standard gesetzt ist (Schritt 2 oben), kommt die gesprochene Antwort **automatisch über die Box** — ohne zusätzliche Option. Das ist der Grund, warum die Bluetooth-Umleitung „einfach funktioniert". -- **Eingabe (Aufnahme):** Die Aufnahme folgt **nicht** automatisch dem System-Default — - `voice_loop.py` nimmt mit einem fest gewählten Gerät auf. Damit der **ReSpeaker** genutzt - wird, das Gerät explizit angeben (ReSpeaker = **Karte 6** → `plughw:6,0`): +- **Eingabe (Aufnahme):** `voice_loop.py` folgt mit `--recorder auto` (Standard) ebenfalls + dem **System-Standard-Mikrofon** — es probiert beim Start automatisch ein Werkzeug, das + dem Default folgt und im Kurztest wirklich Audio liefert (`ffmpeg` → `parecord` → `arecord` + → `pw-record`). Stellst du also das Eingabegerät in *Einstellungen → Ton* um (z. B. auf den + ReSpeaker), nutzt der Loop es ohne weitere Option. +- **Bestimmtes Mikrofon erzwingen** (statt System-Default), z. B. den ReSpeaker fix als ALSA- + Gerät (Karte 6): ```bash python scripts/voice_loop.py --recorder arecord --device plughw:6,0 ``` - (Auf diesem Rechner ist `--recorder arecord` mit `plughw:…` der zuverlässige Weg, weil die - Default-folgenden Recorder `pw-record`/`parecord` hier nicht stabil verbinden. Die richtige - Kartennummer notfalls mit `arecord -l` prüfen.) + (Hilfreich, wenn du gezielt ein anderes als das Standard-Mikrofon willst; Kartennummer mit + `arecord -l`. Auf diesem Rechner scheitern `pw-record`/`arecord default` — die `auto`-Probe + überspringt sie automatisch und nimmt `ffmpeg -f pulse`.) -Kurz: **Lautsprecher/Bluetooth umstellen → System-Einstellungen genügen.** -**Mikrofon für den Sprech-Loop → zusätzlich `--device plughw:6,0` mitgeben.** +Kurz: **Mikrofon UND Lautsprecher/Bluetooth umstellen → die System-Einstellungen genügen; +der Sprech-Loop folgt dem Standard automatisch.** `--device` nur, wenn du bewusst abweichen willst. ### Weitere Einstellungen, die du vornehmen kannst - **Ausgabe-Lautstärke / Mikrofon-Empfindlichkeit:** *Ton*-Seite oder @@ -614,7 +617,7 @@ 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 | -| `pw_context_connect() failed` / `arecord: Fehler beim Öffnen des Gerätes` | PipeWire-Client- bzw. ALSA-`default`-Pfad gestört | direktes Gerät nehmen: `arecord -l`, dann `--recorder arecord --device plughw:2,0` | +| `pw_context_connect() failed` / `arecord: Fehler beim Öffnen des Gerätes` | PipeWire-Client- bzw. ALSA-`default`-Pfad gestört | `--recorder auto` (Standard) überspringt tote Werkzeuge automatisch (nutzt `ffmpeg -f pulse`); notfalls direktes Gerät: `arecord -l`, dann `--recorder arecord --device plughw:6,0` | | 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) | diff --git a/scripts/voice_loop.py b/scripts/voice_loop.py index 46331d9..23cd9b8 100644 --- a/scripts/voice_loop.py +++ b/scripts/voice_loop.py @@ -5,16 +5,19 @@ Nimmt vom Mikrofon auf (Push-to-Talk), schickt das Audio ueber EINE /ws/voice-Verbindung an das Gateway und spielt die Antwort ab. Das Gespraechs- gedaechtnis bleibt ueber die `session_id` erhalten. +Standardmaessig folgt der Loop dem **System-Standard-Mikrofon und -Lautsprecher** +(inkl. Bluetooth). Ein bestimmtes Geraet nur, wenn `--device` explizit gesetzt ist. + Beispiele: - python scripts/voice_loop.py + python scripts/voice_loop.py # System-Standardgeraete python scripts/voice_loop.py --url ws://localhost:8003/ws/voice --session oma-anna - python scripts/voice_loop.py --device hw:1,0 # bestimmtes Mikrofon (arecord -L) + python scripts/voice_loop.py --recorder arecord --device plughw:6,0 # bestimmtes Mikrofon python scripts/voice_loop.py --llm-provider openrouter --tts-provider openrouter python scripts/voice_loop.py --tts-provider openrouter --voice Puck # Cloud-Stimme testen python scripts/voice_loop.py --file frage.wav # ohne Mikrofon (Test) -Voraussetzungen: laufendes Gateway, `arecord` (Aufnahme), ein Player -(`ffplay`/`aplay`/`paplay`), Python-Paket `websockets`. +Voraussetzungen: laufendes Gateway, ein Aufnahmewerkzeug (`ffmpeg`/`parecord`/`arecord`), +ein Player (`ffplay`/`paplay`/`aplay`), Python-Paket `websockets`. """ from __future__ import annotations @@ -23,11 +26,13 @@ import argparse import asyncio import io import json +import os import shutil import signal import subprocess import sys import tempfile +import time import wave try: @@ -46,28 +51,73 @@ def _require(tool: str) -> str: def _first_player() -> list[str] | None: + # paplay vor aplay: paplay folgt dem System-Standard-Ausgabegeraet (PulseAudio), + # aplay nutzt das ALSA-Default, das auf PipeWire-Systemen oft tot ist. if shutil.which("ffplay"): return ["ffplay", "-loglevel", "quiet", "-nodisp", "-autoexit"] - if shutil.which("aplay"): - return ["aplay", "-q"] if shutil.which("paplay"): return ["paplay"] + if shutil.which("aplay"): + return ["aplay", "-q"] return None -def resolve_recorder(choice: str) -> str: - """Waehlt das Aufnahmewerkzeug. 'auto' bevorzugt PipeWire (pw-record).""" +def resolve_recorder(choice: str, rate: int = 16000) -> str: + """Waehlt das Aufnahmewerkzeug. + + 'auto' folgt dem **System-Standard-Mikrofon** und prueft per kurzem Test, dass das + Werkzeug WIRKLICH Audio liefert -> es wird nie ein totes Geraet gewaehlt. Reihenfolge + der Kandidaten: ffmpeg (PulseAudio-Default) -> parecord -> arecord -> 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): + available = [t for t in ("ffmpeg", "parecord", "arecord", "pw-record") if shutil.which(t)] + if not available: + sys.exit("Kein Aufnahmewerkzeug gefunden (ffmpeg / parecord / arecord / pw-record).") + print(" Prüfe Standard-Aufnahmegerät …", flush=True) + for tool in available: + if _probe_records(tool, rate): return tool - sys.exit("Kein Aufnahmewerkzeug gefunden (pw-record / parecord / arecord).") + print(f" ⚠ Kein Werkzeug lieferte im Test Audio; nutze '{available[0]}'." + " Ggf. --recorder/--device explizit setzen.") + return available[0] + + +def _probe_records(recorder: str, rate: int) -> bool: + """Kurztest (~0,6 s), ob 'recorder' am System-Default tatsaechlich aufnimmt.""" + tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False) + tmp.close() + size = 0 + try: + proc = subprocess.Popen( + _record_cmd(recorder, None, rate, tmp.name), + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ) + time.sleep(0.6) + proc.send_signal(signal.SIGINT) + try: + proc.wait(timeout=3) + except subprocess.TimeoutExpired: + proc.kill() + size = os.path.getsize(tmp.name) + except Exception: # noqa: BLE001 - jedes Problem = Werkzeug taugt nicht + size = 0 + finally: + try: + os.unlink(tmp.name) + except OSError: + pass + return size > 2000 # mehr als WAV-Header (44 B) + etwas Audio def _record_cmd(recorder: str, device: str | None, rate: int, outfile: str) -> list[str]: + if recorder == "ffmpeg": + # PulseAudio-Eingang folgt dem System-Standard-Mikrofon (device=None -> "default"). + return ["ffmpeg", "-nostdin", "-hide_banner", "-loglevel", "error", "-y", + "-f", "pulse", "-i", device or "default", + "-ar", str(rate), "-ac", "1", outfile] if recorder == "arecord": cmd = ["arecord", "-q", "-f", "S16_LE", "-r", str(rate), "-c", "1"] if device: @@ -158,13 +208,14 @@ def open_stream_player(): waehrend die KI noch weiter generiert. None, wenn kein Player gefunden wird. """ rate = str(TTS_SAMPLE_RATE) + # paplay vor aplay: folgt dem System-Standard-Ausgabegeraet (PulseAudio/PipeWire). if shutil.which("ffplay"): cmd = ["ffplay", "-loglevel", "quiet", "-nodisp", "-autoexit", "-f", "s16le", "-ar", rate, "-ac", "1", "-i", "pipe:0"] - elif shutil.which("aplay"): - cmd = ["aplay", "-q", "-f", "S16_LE", "-r", rate, "-c", "1"] elif shutil.which("paplay"): cmd = ["paplay", "--raw", f"--rate={rate}", "--format=s16le", "--channels=1"] + elif shutil.which("aplay"): + cmd = ["aplay", "-q", "-f", "S16_LE", "-r", rate, "-c", "1"] else: return None return subprocess.Popen(cmd, stdin=subprocess.PIPE) @@ -309,10 +360,10 @@ async def run(args) -> None: await _send_and_play(url, wav, start_frame) return - recorder = resolve_recorder(args.recorder) + recorder = resolve_recorder(args.recorder, args.rate) print(f"Ziel: {args.url} (Session '{args.session}')") print(f"Aufnahme mit: {recorder}" - + (f" (Gerät: {args.device})" if args.device else " (Standardgerät)")) + + (f" (Gerät: {args.device})" if args.device else " (System-Standardgerät)")) print("Sprich nach 'START', stoppe mit Enter. Strg+C beendet den Loop.") while True: try: @@ -321,11 +372,10 @@ async def run(args) -> None: 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") + print(" ⚠ Keine/zu kurze Aufnahme. Pruefe das System-Standard-Mikrofon" + " (Ubuntu: Einstellungen → Ton → Eingabe) und ob es Pegel zeigt.") + print(" Notfalls direktes ALSA-Geraet erzwingen (arecord -l zeigt die Nummer):") + print(" python scripts/voice_loop.py --recorder arecord --device plughw:6,0") continue try: await _send_and_play(url, wav, start_frame) @@ -339,10 +389,13 @@ def main() -> None: 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 (arecord: -L; pw-record: --target; parecord: --device)") + help="Aufnahmegeraet explizit (ffmpeg/parecord: PulseAudio-Quelle; " + "arecord: ALSA z. B. plughw:6,0; pw-record: --target). " + "Ohne Angabe folgt der Loop dem System-Standardgeraet.") p.add_argument("--recorder", default="auto", - choices=["auto", "pw-record", "parecord", "arecord"], - help="Aufnahmewerkzeug; 'auto' bevorzugt PipeWire (pw-record)") + choices=["auto", "ffmpeg", "pw-record", "parecord", "arecord"], + help="Aufnahmewerkzeug; 'auto' folgt dem System-Standardmikrofon und " + "prueft per Kurztest, dass es wirklich aufnimmt") 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)