ux(voice_loop): Aufnahmedauer anzeigen + korrekt berechnen; Diagnose-Skript

- record_utterance zeigt nach der Aufnahme die Dauer
- Dauer aus tatsaechlicher Datenmenge statt WAV-Header (arecord schreibt beim
  Stoppen per Signal eine falsche Header-Laenge -> sonst absurde Werte wie 67108s)
- scripts/diag_record.py: Countdown + gruenes Signal -> Aufnahme -> sofort
  transkribieren (Standard + vad_filter) zur Pausen-Diagnose

Diagnose-Ergebnis: Mehrsatz-Aeusserungen mit Pausen werden vollstaendig erkannt
(Aufnahme + faster-whisper verlieren nichts) - im kontrollierten Test und im
echten voice_loop-Flow bestaetigt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-17 15:53:51 +02:00
commit b75382e055
2 changed files with 86 additions and 0 deletions

73
scripts/diag_record.py Normal file
View file

@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Diagnose: Countdown + grünes Signal -> Aufnahme -> sofort transkribieren.
Ein Durchlauf, kein Timing-Problem, keine Verwechslung der Datei. Zeigt die
WAV-Dauer und die faster-whisper-Segmente (Standard und mit VAD-Filter), damit man
sieht, ob Sätze nach Pausen verloren gehen.
.venv/bin/python scripts/diag_record.py
.venv/bin/python scripts/diag_record.py --device plughw:5,0 --seconds 20 --model base
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
import time
import wave
GREEN = "\033[1;32m"
RED = "\033[1;31m"
RESET = "\033[0m"
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--device", default="plughw:5,0", help="Aufnahmegerät (arecord -L)")
ap.add_argument("--seconds", type=int, default=20)
ap.add_argument("--rate", type=int, default=16000)
ap.add_argument("--model", default="base", help="tiny|base|small|medium|large-v3")
args = ap.parse_args()
if not shutil.which("arecord"):
sys.exit("Fehlt: arecord")
wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
wav.close()
print()
for n in (3, 2, 1):
print(f" Aufnahme startet in {n}", end="\r", flush=True)
time.sleep(1)
print(f"\n\n{GREEN}🟢🟢🟢🟢🟢 S P R I C H J E T Z T ({args.seconds}s) 🟢🟢🟢🟢🟢{RESET}\n")
result = subprocess.run(
["arecord", "-q", "-f", "S16_LE", "-r", str(args.rate), "-c", "1",
"-D", args.device, "-d", str(args.seconds), wav.name]
)
print(f"{RED}🔴 STOP — Aufnahme fertig.{RESET}\n")
if result.returncode != 0:
sys.exit(f"arecord-Fehler (Gerät {args.device}?). Geräte: arecord -L")
with wave.open(wav.name, "rb") as w:
dur = w.getnframes() / w.getframerate()
print(f"WAV-Dauer: {dur:.1f}s (Datei: {wav.name})\n")
print("Lade faster-whisper …")
from faster_whisper import WhisperModel
model = WhisperModel(args.model, device="cpu", compute_type="int8")
for title, kw in [("Standard (wie im Provider)", {}), ("mit vad_filter=True", {"vad_filter": True})]:
print(f"--- {title} ---")
segments, _ = model.transcribe(wav.name, language="de", **kw)
count = 0
for seg in segments:
count += 1
print(f" [{seg.start:5.1f}-{seg.end:5.1f}] {seg.text.strip()!r}")
if count == 0:
print(" (nichts erkannt)")
if __name__ == "__main__":
main()

View file

@ -109,6 +109,19 @@ def record_utterance(recorder: str, device: str | None, rate: int) -> bytes:
with open(tmp.name, "rb") as fh:
data = fh.read()
# Aufnahmedauer anzeigen (hilft, zu fruehes Stoppen sofort zu erkennen).
# Hinweis: arecord schreibt beim Stoppen per Signal eine falsche Laenge in den
# WAV-Header -> Dauer aus der TATSAECHLICHEN Datenmenge berechnen, nicht aus dem Header.
try:
with wave.open(io.BytesIO(data), "rb") as wav_in:
rate = wav_in.getframerate() or 16000
channels = wav_in.getnchannels() or 1
width = wav_in.getsampwidth() or 2
raw = wav_in.readframes(10 ** 9) # liest alles Vorhandene (Header-Laenge unzuverlaessig)
seconds = len(raw) / (rate * channels * width)
print(f" Aufnahme: {seconds:.1f}s")
except (wave.Error, EOFError):
pass
# Recorder-Fehlertext nur zeigen, wenn nichts/zu wenig aufgenommen wurde.
if len(data) < 1000:
err.seek(0)