STT-Dienst: Kern, HTTP/WebSocket/Wyoming-Service, CLI, Tests, systemd-Unit

- stt_core.py: Whisper-Engine (einmal laden, CPU-Fallback), Audio-Dekodierung
  jedes Formats, Halluzinationsfilter, Äußerungserkennung per Silero-VAD
- stt_service.py: /health, /v1/audio/transcriptions (OpenAI), /transcribe (Rohdaten),
  WS /ws/stream; Inferenz im Worker-Thread, begrenzte Warteschlange (503)
- stt_wyoming.py: Home-Assistant-Anbindung (TCP 10300)
- stt_cli.py: Datei/Mikrofon über den Dienst oder lokal
- Tests (Unit + Integration), README, requirements.txt, deploy/stt-service.service

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-09-13 21:34:03 +02:00
commit a986cbe42a

127
README.md Normal file
View file

@ -0,0 +1,127 @@
# SpeechToText
Lokaler, schneller Spracherkennungs-Dienst (Sprache → Text) auf Basis von
[faster-whisper](https://github.com/SYSTRAN/faster-whisper) — das STT-Gegenstück zu
[chatterbox-tts-cli](https://kitux.de/forgejo/dschlueter/chatterbox-tts-cli).
Gedacht als Baustein für Sprachassistenten und Smarthome: Home Assistant (Wyoming),
Mikrocontroller wie ESP32 (HTTP/WebSocket), Skripte (CLI) und OpenAI-kompatible Tools.
- **Schnell:** Modell bleibt geladen. Gemessen auf RTX 3090 mit `large-v3-turbo`:
5,8 s Audio → 0,2 s, 35 s Audio → 0,7 s Rechenzeit; ~2,4 GB VRAM.
- **Nicht blockierend:** Inferenz in einem Worker-Thread, Warteschlange begrenzt (503 statt Stau).
- **Äußerungsende per VAD** (Silero) statt Stoppwort; Transkript ca. 1 s nach Sprechende.
- **Robust:** GPU-Fehler → automatischer CPU-Fallback; beliebige Audioformate; Halluzinationsfilter
(„Untertitel im Auftrag des ZDF“ …).
## Installation
```bash
python3 -m venv --system-site-packages .venv # nutzt vorhandenes faster-whisper/torch-Umfeld
.venv/bin/pip install -r requirements.txt
```
## Dienst starten
```bash
# Manuell (GPU per UUID pinnen: nvidia-smi -L)
CUDA_VISIBLE_DEVICES=GPU-<uuid> .venv/bin/uvicorn stt_service:app --host 0.0.0.0 --port 9998
# Als systemd-User-Service (Autostart)
cp deploy/stt-service.service ~/.config/systemd/user/
systemctl --user daemon-reload && systemctl --user enable --now stt-service
journalctl --user -u stt-service -f
curl http://127.0.0.1:9998/health
```
Beim ersten Start wird das Modell heruntergeladen (`large-v3-turbo` ≈ 1,6 GB). Der Dienst ist sofort
erreichbar; `/health` meldet `loading`, bis das Modell bereit ist.
### Konfiguration (Umgebungsvariablen)
| Variable | Standard | Bedeutung |
|---|---|---|
| `STT_MODEL` | `large-v3-turbo` | Whisper-Modell (`tiny` … `large-v3`, `large-v3-turbo`, Pfad) |
| `STT_DEVICE` | `auto` | `auto` / `cuda` / `cpu` |
| `STT_COMPUTE_TYPE` | `auto` | `float16` (GPU) / `int8` (CPU) automatisch |
| `STT_CPU_MODEL` | `small` | Modell beim Ausweichen auf CPU |
| `STT_LANGUAGE` | `de` | Default-Sprache, `auto` = Erkennung |
| `STT_BEAM_SIZE` | `5` | Beam-Search-Breite (1 = schneller) |
| `STT_INITIAL_PROMPT` | – | Kontext/Eigennamen für Whisper |
| `STT_MAX_QUEUE` | `8` | max. wartende Aufträge, darüber HTTP 503 |
| `STT_MAX_UPLOAD_MB` | `50` | max. Uploadgröße |
| `STT_PRELOAD` | `1` | Modell beim Start laden + aufwärmen |
| `STT_VAD_SILENCE_MS` | `700` | Stille bis Äußerungsende (Streaming) |
| `STT_VAD_THRESHOLD` | `0.5` | Silero-Sprachschwelle |
| `STT_VAD_MAX_UTTERANCE_S` | `30` | max. Äußerungslänge |
| `STT_WYOMING_HOST` / `STT_WYOMING_PORT` | `0.0.0.0` / `10300` | Wyoming-Server, Port leer = aus |
| `STT_LOG_LEVEL` | `INFO` | Log-Level |
## Schnittstellen
### HTTP
```bash
# OpenAI-kompatibel (multipart) — response_format: json | text | verbose_json
curl -F file=@aufnahme.mp3 -F language=de http://127.0.0.1:9998/v1/audio/transcriptions
# Rohdaten im Body — ideal für Mikrocontroller (kein multipart nötig)
curl --data-binary @aufnahme.wav http://127.0.0.1:9998/transcribe
curl --data-binary @roh.pcm "http://127.0.0.1:9998/transcribe?format=pcm&sample_rate=16000&channels=1"
```
`/transcribe` liefert `text`, `language`, `duration`, `processing_s`, `segments`.
### WebSocket `/ws/stream` (Live-Streaming)
Client schickt s16le-PCM in beliebig großen Stücken; der Dienst erkennt Äußerungen selbst.
```text
→ {"type":"start","sample_rate":16000,"channels":1,"channel":null,"language":"de","vad":true,"silence_ms":700}
→ <binär: PCM> …
← {"type":"speech_start","t":1.31}
← {"type":"transcript","text":"Schalte das Licht ein.","language":"de","duration":2.4,"processing_s":0.21,"reason":"silence","t":3.9}
→ {"type":"end"} # laufende Äußerung abschließen → danach {"type":"done"}
→ {"type":"close"} # ausstehende Transkripte senden, dann schließen
```
Ohne VAD (`"vad":false`) wird alles bis `end` gesammelt und als Ganzes transkribiert (Push-to-Talk).
Für ESP32: I²S-Mikrofon mit 16 kHz mono lesen und die Puffer direkt als Binär-Frames senden.
### Home Assistant (Wyoming)
Einstellungen → Geräte & Dienste → Integration hinzufügen → **Wyoming Protocol** →
Host = dieser Rechner, Port `10300`. Danach in der Sprachassistenten-Pipeline als
Spracherkennung auswählen. ESPHome-Satelliten nutzen den Dienst dann automatisch.
## CLI
Nur Transkripte auf stdout, Status auf stderr → skriptfähig.
```bash
.venv/bin/python stt_cli.py aufnahme.wav # Datei über den Dienst
.venv/bin/python stt_cli.py *.mp3 --json # JSON-Zeilen
.venv/bin/python stt_cli.py # Mikrofon (System-Default), fortlaufend
text=$(.venv/bin/python stt_cli.py --once) # eine Äußerung → Variable
.venv/bin/python stt_cli.py -d respeaker -v # bestimmtes Gerät (ReSpeaker: ASR-Kanal automatisch)
.venv/bin/python stt_cli.py --local --model small aufnahme.wav # ohne Dienst
.venv/bin/python stt_cli.py --list-devices
```
Exit-Codes: `0` ok, `1` Fehler, `2` keine Sprache erkannt.
## Tests
```bash
.venv/bin/python -m pytest # schnell, ohne Modell (Fake-Engine)
.venv/bin/python -m pytest -m integration # echtes Whisper (small, CPU) + Silero mit espeak-ng-Sprache
.venv/bin/python -m pytest tests/test_core.py::test_segmenter_detects_two_utterances
```
## Bekannte Grenzen
- Wörtlich wiederholte Sätze innerhalb eines 30-s-Fensters fasst Whisper teils zusammen
(bei natürlicher Sprache selten).
- Keine wortweisen Zwischenergebnisse während des Sprechens — Transkript kommt pro Äußerung.
- `stt.py` und `stt_respeaker.py` sind die alten Einzelskripte (historisch).