Dokumentation: Bedienungsanleitung, API-Referenz, Entwicklerdoku, README überarbeitet
- BEDIENUNGSANLEITUNG.md: Installation, Betrieb, CLI, Mikrofon, Home Assistant, Einstellungen, Modellwahl, Fehlerbehebung, Datenschutz - docs/API.md: HTTP, WebSocket-Protokoll, Wyoming, Fehlercodes, geprüfte Beispiele - docs/ENTWICKLER.md: Architektur, Module, Nebenläufigkeit, Tests, Erweitern, Designentscheidungen - CLAUDE.md: Verweis auf die Dokumentation Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7a4d0e2885
commit
340100210d
5 changed files with 1430 additions and 89 deletions
434
BEDIENUNGSANLEITUNG.md
Normal file
434
BEDIENUNGSANLEITUNG.md
Normal file
|
|
@ -0,0 +1,434 @@
|
||||||
|
# Bedienungsanleitung SpeechToText
|
||||||
|
|
||||||
|
Diese Anleitung richtet sich an **Anwender**: Installieren, Starten, Benutzen, Einrichten mit Home
|
||||||
|
Assistant, Probleme lösen. Für Programmierer: [docs/API.md](docs/API.md) (Schnittstellen) und
|
||||||
|
[docs/ENTWICKLER.md](docs/ENTWICKLER.md) (Aufbau des Codes).
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
1. [Was macht das Programm?](#1-was-macht-das-programm)
|
||||||
|
2. [Voraussetzungen](#2-voraussetzungen)
|
||||||
|
3. [Installation](#3-installation)
|
||||||
|
4. [Dienst starten und prüfen](#4-dienst-starten-und-prüfen)
|
||||||
|
5. [Benutzung mit der Kommandozeile (CLI)](#5-benutzung-mit-der-kommandozeile-cli)
|
||||||
|
6. [Das richtige Mikrofon](#6-das-richtige-mikrofon)
|
||||||
|
7. [Home Assistant anbinden](#7-home-assistant-anbinden)
|
||||||
|
8. [Andere Geräte im Netzwerk](#8-andere-geräte-im-netzwerk)
|
||||||
|
9. [Einstellungen](#9-einstellungen)
|
||||||
|
10. [Welches Whisper-Modell?](#10-welches-whisper-modell)
|
||||||
|
11. [Tipps für gute Erkennung](#11-tipps-für-gute-erkennung)
|
||||||
|
12. [Fehlerbehebung](#12-fehlerbehebung)
|
||||||
|
13. [Datenschutz](#13-datenschutz)
|
||||||
|
14. [Deinstallation](#14-deinstallation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Was macht das Programm?
|
||||||
|
|
||||||
|
SpeechToText wandelt **gesprochene Sprache in Text** um — vollständig **lokal** auf dem eigenen Rechner,
|
||||||
|
ohne Cloud. Grundlage ist das Spracherkennungsmodell *Whisper* in der schnellen Umsetzung
|
||||||
|
*faster-whisper*.
|
||||||
|
|
||||||
|
Es besteht aus zwei Teilen:
|
||||||
|
|
||||||
|
- **Dem Dienst** (`stt_service.py`): läuft im Hintergrund, hält das Sprachmodell im Speicher und nimmt
|
||||||
|
Aufträge entgegen — von Home Assistant, von Mikrocontrollern, von Skripten oder von der CLI.
|
||||||
|
- **Der Kommandozeile** (`stt_cli.py`): schickt Audiodateien oder das Mikrofon an den Dienst und gibt den
|
||||||
|
erkannten Text aus.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
MIC[Mikrofon] --> CLI[stt_cli.py]
|
||||||
|
FILE[Audiodatei] --> CLI
|
||||||
|
HA[Home Assistant] -- Wyoming :10300 --> SVC
|
||||||
|
ESP[ESP32 / Skripte / Apps] -- HTTP / WebSocket :9998 --> SVC
|
||||||
|
CLI -- HTTP / WebSocket :9998 --> SVC[STT-Dienst<br/>Whisper-Modell auf GPU]
|
||||||
|
SVC --> TXT[Text]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wie schnell?** Auf einer RTX 3090 mit dem Modell `large-v3-turbo`: ein Satz von 6 Sekunden ist in
|
||||||
|
0,2 Sekunden erkannt, 35 Sekunden Sprache in 0,7 Sekunden. Beim freien Sprechen erscheint der Text etwa
|
||||||
|
eine Sekunde, nachdem man aufgehört hat zu sprechen.
|
||||||
|
|
||||||
|
**Wie merkt das Programm, dass ich fertig bin?** Es erkennt eine **Sprechpause** (Standard: 0,7 s Stille
|
||||||
|
nach Sprache). Ein Stoppwort ist nicht nötig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Voraussetzungen
|
||||||
|
|
||||||
|
| | Minimum | Empfohlen |
|
||||||
|
|---|---|---|
|
||||||
|
| Betriebssystem | Linux (getestet: Ubuntu mit PipeWire) | |
|
||||||
|
| Python | 3.11 | 3.12 (getestet) |
|
||||||
|
| Grafikkarte | keine (läuft auch auf CPU, dann langsamer) | NVIDIA mit ≥ 4 GB freiem Speicher, CUDA 12 |
|
||||||
|
| Arbeitsspeicher | 4 GB frei | 8 GB frei |
|
||||||
|
| Festplatte | 0,5 GB (Modell `small`) | 2 GB (Modell `large-v3-turbo`) |
|
||||||
|
| Mikrofon | beliebig (nur für Live-Betrieb) | ReSpeaker XVF3800 oder gutes USB-Mikrofon |
|
||||||
|
|
||||||
|
Systemprogramme: `ffmpeg` ist hilfreich (Formatumwandlung), `pactl` (PipeWire/PulseAudio) für die
|
||||||
|
Mikrofonwahl, `espeak-ng` nur für die automatischen Tests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Python_Programs
|
||||||
|
git clone <repository-url> SpeechToText # bzw. vorhandenen Ordner nutzen
|
||||||
|
cd SpeechToText
|
||||||
|
|
||||||
|
python3 -m venv --system-site-packages .venv
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
`--system-site-packages` nutzt bereits installierte Pakete (z. B. faster-whisper, CUDA-Bibliotheken)
|
||||||
|
mit. Ohne vorhandene Installation lädt `pip` alles herunter.
|
||||||
|
|
||||||
|
**Installation prüfen** (dauert ca. 10 Sekunden, braucht kein Modell):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m pytest
|
||||||
|
# … 36 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
> Beim **ersten Start des Dienstes** wird das Whisper-Modell aus dem Internet geladen
|
||||||
|
> (`large-v3-turbo` ≈ 1,6 GB). Danach arbeitet alles offline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Dienst starten und prüfen
|
||||||
|
|
||||||
|
### Manuell starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Python_Programs/SpeechToText
|
||||||
|
.venv/bin/python stt_service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf Rechnern mit **mehreren Grafikkarten** die gewünschte Karte fest angeben. Die Kennung (UUID) zeigt
|
||||||
|
`nvidia-smi -L`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CUDA_VISIBLE_DEVICES=GPU-83ba6d1f-ebbe-f8c8-1433-153e0ebc3866 .venv/bin/python stt_service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
> Warum UUID statt Nummer? `nvidia-smi` und CUDA nummerieren die Karten unterschiedlich. Mit der UUID
|
||||||
|
> landet der Dienst sicher auf der richtigen Karte.
|
||||||
|
|
||||||
|
Im Terminal erscheint u. a.:
|
||||||
|
|
||||||
|
```
|
||||||
|
INFO stt.service: Wyoming-Server auf tcp://0.0.0.0:10300
|
||||||
|
INFO stt: Lade Whisper-Modell large-v3-turbo (cuda, float16)
|
||||||
|
INFO stt: Modell bereit nach 2.3s
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Fenster offen lassen. **Beenden** mit `Strg+C`.
|
||||||
|
|
||||||
|
### Prüfen
|
||||||
|
|
||||||
|
In einem zweiten Terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:9998/health
|
||||||
|
```
|
||||||
|
|
||||||
|
`"status":"ok"` = bereit. `"status":"loading"` = Modell lädt noch (kurz warten).
|
||||||
|
`"device":"cuda"` = läuft auf der Grafikkarte, `"cpu"` = auf dem Prozessor.
|
||||||
|
|
||||||
|
Im Browser zeigt **http://127.0.0.1:9998/docs** alle Schnittstellen zum Ausprobieren.
|
||||||
|
|
||||||
|
### Optional: automatisch starten (systemd)
|
||||||
|
|
||||||
|
Soll der Dienst dauerhaft laufen (nötig z. B. für Home Assistant nach einem Neustart):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp deploy/stt-service.service ~/.config/systemd/user/
|
||||||
|
# ggf. CUDA_VISIBLE_DEVICES und Einstellungen in der Datei anpassen
|
||||||
|
systemctl --user daemon-reload
|
||||||
|
systemctl --user enable --now stt-service
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aufgabe | Befehl |
|
||||||
|
|---|---|
|
||||||
|
| Status | `systemctl --user status stt-service` |
|
||||||
|
| Neustart (nach Änderungen) | `systemctl --user restart stt-service` |
|
||||||
|
| Anhalten | `systemctl --user stop stt-service` |
|
||||||
|
| Autostart abschalten | `systemctl --user disable --now stt-service` |
|
||||||
|
| Live-Log | `journalctl --user -u stt-service -f` |
|
||||||
|
|
||||||
|
Vorher einen manuell gestarteten Dienst beenden — beide wollen Port 9998.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Benutzung mit der Kommandozeile (CLI)
|
||||||
|
|
||||||
|
Die CLI gibt **nur den erkannten Text** aus (Standardausgabe). Statusmeldungen stehen getrennt davon
|
||||||
|
(Fehlerausgabe). Deshalb lässt sie sich direkt in Skripten verwenden.
|
||||||
|
|
||||||
|
Alle Beispiele im Projektordner: `cd ~/Python_Programs/SpeechToText`
|
||||||
|
|
||||||
|
### Audiodateien
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python stt_cli.py aufnahme.wav
|
||||||
|
.venv/bin/python stt_cli.py interview.mp3 notiz.ogg # mehrere Dateien, eine Zeile je Datei
|
||||||
|
.venv/bin/python stt_cli.py aufnahme.wav --json # mit Details
|
||||||
|
.venv/bin/python stt_cli.py aufnahme.wav > text.txt # in Datei speichern
|
||||||
|
```
|
||||||
|
|
||||||
|
Unterstützt: WAV, MP3, OGG, FLAC, M4A, WebM und praktisch alle anderen Audioformate.
|
||||||
|
|
||||||
|
### Mikrofon
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python stt_cli.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach **„Bereit — sprich.“** einfach sprechen und kurz Pause machen. Jeder Satz erscheint als eigene
|
||||||
|
Zeile. Beenden mit `Strg+C`.
|
||||||
|
|
||||||
|
```
|
||||||
|
Mikrofon: pulse → alsa_input.usb-Seeed_Studio_reSpeaker_XVF3800_… (16000 Hz, 2 Kanäle, Kanal 1)
|
||||||
|
Bereit — sprich. (Strg+C beendet)
|
||||||
|
… Sprache erkannt (2.3s)
|
||||||
|
(3.1s Audio, 0.21s Rechenzeit)
|
||||||
|
Schalte das Licht im Wohnzimmer ein.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alle Optionen
|
||||||
|
|
||||||
|
| Option | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `DATEI …` | Audiodateien transkribieren; ohne Datei wird das Mikrofon benutzt |
|
||||||
|
| `-v`, `--verbose` | Statusdetails anzeigen (Sprachbeginn, Dauer, Rechenzeit) |
|
||||||
|
| `--json` | Ergebnis als JSON-Zeile (Text, Sprache, Dauer, Rechenzeit …) |
|
||||||
|
| `-l`, `--language` | Sprache: `de`, `en`, `fr` … oder `auto` (automatisch erkennen) |
|
||||||
|
| `--prompt "…"` | Eigennamen/Fachbegriffe vorgeben, z. B. `--prompt "Bredelar, Marsberg"` |
|
||||||
|
| `--once` | nach dem ersten erkannten Satz beenden |
|
||||||
|
| `--timeout SEK` | Mikrofon nach SEK Sekunden beenden |
|
||||||
|
| `--silence-ms MS` | Pausenlänge, die einen Satz beendet (Standard 700; für Denkpausen 1200–2000) |
|
||||||
|
| `-d`, `--device` | Mikrofon wählen: Nummer oder Namensteil, z. B. `-d respeaker` |
|
||||||
|
| `--channel N` | Mikrofonkanal wählen (ReSpeaker: automatisch 1) |
|
||||||
|
| `--list-devices` | verfügbare Mikrofone anzeigen (`*` = Standard von PortAudio) |
|
||||||
|
| `--url URL` | anderer Dienst, z. B. `--url http://192.168.1.10:9998` (auch per `STT_URL`) |
|
||||||
|
| `--local` | **ohne Dienst**: Modell direkt laden (Start dauert länger) |
|
||||||
|
| `--model NAME` | nur mit `--local`: Whisper-Modell, z. B. `small`, `large-v3-turbo` |
|
||||||
|
| `--compute-device` | nur mit `--local`: `auto`, `cuda` oder `cpu` |
|
||||||
|
| `--http-timeout SEK` | max. Wartezeit bei Dateien (Standard 300) |
|
||||||
|
|
||||||
|
### Rückgabewerte (für Skripte)
|
||||||
|
|
||||||
|
| Code | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `0` | Text erkannt |
|
||||||
|
| `1` | Fehler (Dienst nicht erreichbar, Datei fehlt …) |
|
||||||
|
| `2` | keine Sprache erkannt (nur Stille/Geräusche, Zeitlimit abgelaufen) |
|
||||||
|
| `130` | mit `Strg+C` abgebrochen, bevor ein Ergebnis vorlag (z. B. während einer Dateiübertragung) |
|
||||||
|
|
||||||
|
### Beispiele für Skripte
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Einen Satz sprechen und in einer Variablen weiterverarbeiten
|
||||||
|
text=$(.venv/bin/python stt_cli.py --once --timeout 15)
|
||||||
|
[ $? -eq 0 ] && echo "Du hast gesagt: $text"
|
||||||
|
|
||||||
|
# Alle MP3s eines Ordners in Textdateien umwandeln
|
||||||
|
for f in aufnahmen/*.mp3; do
|
||||||
|
.venv/bin/python stt_cli.py "$f" > "${f%.mp3}.txt"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Diktat mit Denkpausen fortlaufend in eine Datei schreiben
|
||||||
|
.venv/bin/python stt_cli.py --silence-ms 1500 >> notizen.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ohne laufenden Dienst
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python stt_cli.py --local --model large-v3-turbo aufnahme.wav
|
||||||
|
.venv/bin/python stt_cli.py --local --model small --compute-device cpu -v # Mikrofon auf CPU
|
||||||
|
```
|
||||||
|
|
||||||
|
Praktisch für gelegentliche Nutzung; jeder Aufruf lädt das Modell aber neu (Sekunden bis Minuten).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Das richtige Mikrofon
|
||||||
|
|
||||||
|
**Standard:** Ohne `-d` benutzt die CLI das Mikrofon, das in den **Systemeinstellungen** (Ubuntu:
|
||||||
|
Einstellungen → Ton → Eingabe) ausgewählt ist. Die erste Statuszeile zeigt, welches:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mikrofon: pulse → alsa_input.usb-Seeed_Studio_reSpeaker_XVF3800_…
|
||||||
|
```
|
||||||
|
|
||||||
|
**Anderes Mikrofon:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python stt_cli.py --list-devices # Liste mit Nummern
|
||||||
|
.venv/bin/python stt_cli.py -d 15 -v # per Nummer
|
||||||
|
.venv/bin/python stt_cli.py -d respeaker -v # per Namensteil
|
||||||
|
```
|
||||||
|
|
||||||
|
**ReSpeaker XVF3800:** Das Gerät liefert zwei Kanäle. Der **rechte Kanal (1)** ist speziell für
|
||||||
|
Spracherkennung aufbereitet (Echo- und Rauschunterdrückung). Die CLI erkennt den ReSpeaker am Namen und
|
||||||
|
wählt Kanal 1 automatisch. Andere Mikrofone: beide Kanäle werden gemischt; mit `--channel 0`/`1` lässt
|
||||||
|
sich ein Kanal erzwingen.
|
||||||
|
|
||||||
|
**Pegel prüfen:** In den Toneinstellungen muss der Eingangspegel beim Sprechen ausschlagen. Wird nie
|
||||||
|
„Sprache erkannt“ angezeigt, ist das Mikrofon meist falsch gewählt, stumm oder zu leise.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Home Assistant anbinden
|
||||||
|
|
||||||
|
Der Dienst spricht das **Wyoming-Protokoll**, mit dem Home Assistant lokale Spracherkennung einbindet.
|
||||||
|
|
||||||
|
1. Dienst dauerhaft laufen lassen ([systemd](#optional-automatisch-starten-systemd)), sonst fehlt die
|
||||||
|
Spracherkennung nach einem Neustart.
|
||||||
|
2. Port 10300 im Netzwerk erreichbar machen (siehe [Abschnitt 8](#8-andere-geräte-im-netzwerk)).
|
||||||
|
3. In Home Assistant: **Einstellungen → Geräte & Dienste → Integration hinzufügen → „Wyoming Protocol“**
|
||||||
|
4. **Host:** IP-Adresse des STT-Rechners, **Port:** `10300` → Absenden. Es erscheint „speech-to-text“.
|
||||||
|
5. **Einstellungen → Sprachassistenten** → Assistent öffnen → **Spracherkennung:** „speech-to-text“,
|
||||||
|
Sprache „Deutsch“.
|
||||||
|
6. Testen, z. B. über das Mikrofon-Symbol in der Home-Assistant-App oder einen ESPHome-Satelliten
|
||||||
|
(z. B. ESP32-S3-Box, M5Stack Atom Echo).
|
||||||
|
|
||||||
|
Home Assistant bzw. der Satellit erkennt das Satzende selbst und schickt die komplette Aufnahme. Im Log
|
||||||
|
des Dienstes erscheint pro Befehl eine Zeile `Wyoming-Transkript (…)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Andere Geräte im Netzwerk
|
||||||
|
|
||||||
|
Der Dienst ist im ganzen Netzwerk erreichbar unter `http://<IP-des-Rechners>:9998` (HTTP/WebSocket) und
|
||||||
|
`<IP>:10300` (Wyoming). Die IP zeigt `hostname -I`.
|
||||||
|
|
||||||
|
Ist eine Firewall aktiv, die Ports für das Heimnetz freigeben:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ufw allow from 192.168.1.0/24 to any port 9998 proto tcp comment 'SpeechToText HTTP'
|
||||||
|
sudo ufw allow from 192.168.1.0/24 to any port 10300 proto tcp comment 'SpeechToText Wyoming'
|
||||||
|
```
|
||||||
|
|
||||||
|
(`192.168.1.0/24` an das eigene Netz anpassen.)
|
||||||
|
|
||||||
|
> **Wichtig:** Der Dienst hat **kein Passwort**. Jeder im Netzwerk kann Audio schicken. Die Ports daher
|
||||||
|
> **nicht ins Internet** freigeben (keine Portweiterleitung im Router).
|
||||||
|
|
||||||
|
Wie Mikrocontroller (ESP32), Web-Apps oder eigene Programme den Dienst ansprechen, beschreibt
|
||||||
|
[docs/API.md](docs/API.md) mit Beispielen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Einstellungen
|
||||||
|
|
||||||
|
Der Dienst wird über **Umgebungsvariablen** eingestellt:
|
||||||
|
|
||||||
|
- **manuell:** vor den Startbefehl schreiben, z. B.
|
||||||
|
`STT_MODEL=small STT_LANGUAGE=en .venv/bin/python stt_service.py`
|
||||||
|
- **systemd:** in `~/.config/systemd/user/stt-service.service` als `Environment="STT_MODEL=small"`,
|
||||||
|
danach `systemctl --user daemon-reload && systemctl --user restart stt-service`
|
||||||
|
|
||||||
|
| Variable | Standard | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| `STT_MODEL` | `large-v3-turbo` | Whisper-Modell, siehe [Abschnitt 10](#10-welches-whisper-modell) |
|
||||||
|
| `STT_LANGUAGE` | `de` | Standardsprache; `auto` = automatisch erkennen |
|
||||||
|
| `STT_DEVICE` | `auto` | `auto` (GPU wenn vorhanden), `cuda` oder `cpu` |
|
||||||
|
| `STT_COMPUTE_TYPE` | `auto` | Rechengenauigkeit; `auto` = `float16` auf GPU, `int8` auf CPU |
|
||||||
|
| `STT_CPU_MODEL` | `small` | Modell, falls die GPU nicht nutzbar ist und auf CPU ausgewichen wird |
|
||||||
|
| `STT_CPU_THREADS` | `0` | CPU-Threads (0 = automatisch) |
|
||||||
|
| `STT_BEAM_SIZE` | `5` | Suchbreite; `1` ist schneller, `5` genauer |
|
||||||
|
| `STT_INITIAL_PROMPT` | – | Eigennamen/Fachbegriffe für alle Anfragen |
|
||||||
|
| `STT_PRELOAD` | `1` | Modell sofort beim Start laden (`0` = erst bei der ersten Anfrage) |
|
||||||
|
| `STT_MAX_QUEUE` | `8` | max. gleichzeitig wartende Aufträge, darüber Ablehnung („beschäftigt“) |
|
||||||
|
| `STT_MAX_UPLOAD_MB` | `50` | größte erlaubte Audiodatei |
|
||||||
|
| `STT_VAD_SILENCE_MS` | `700` | Pause, die beim Live-Streaming einen Satz beendet |
|
||||||
|
| `STT_VAD_THRESHOLD` | `0.5` | Empfindlichkeit der Spracherkennung (höher = weniger Fehlauslösung durch Geräusche) |
|
||||||
|
| `STT_VAD_MIN_SPEECH_MS` | `200` | Mindestlänge von Sprache (filtert Klicks und Klopfen) |
|
||||||
|
| `STT_VAD_PRE_ROLL_MS` | `300` | Audio vor dem erkannten Sprachbeginn mitnehmen |
|
||||||
|
| `STT_VAD_MAX_UTTERANCE_S` | `30` | längster Satz; längere Abschnitte werden geteilt |
|
||||||
|
| `STT_WYOMING_PORT` | `10300` | Port für Home Assistant; leer = aus |
|
||||||
|
| `STT_WYOMING_HOST` | `0.0.0.0` | Netzwerkschnittstelle für Wyoming |
|
||||||
|
| `STT_HOST` / `STT_PORT` | `0.0.0.0` / `9998` | Adresse des HTTP-Dienstes (nur bei `python stt_service.py`) |
|
||||||
|
| `STT_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
||||||
|
| `STT_DOWNLOAD_ROOT` | – | eigener Ordner für Modelle (sonst Hugging-Face-Cache) |
|
||||||
|
| `CUDA_VISIBLE_DEVICES` | – | Grafikkarte festlegen (UUID aus `nvidia-smi -L`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Welches Whisper-Modell?
|
||||||
|
|
||||||
|
| Modell | Qualität Deutsch | Geschwindigkeit | Grafikspeicher | Einsatz |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `tiny` | mäßig | sehr schnell | < 1 GB | nur Tests |
|
||||||
|
| `base` | ausreichend für kurze Befehle | sehr schnell | ~1 GB | schwache Hardware |
|
||||||
|
| `small` | gut | schnell, auch auf CPU brauchbar | ~1 GB | **CPU-Betrieb** |
|
||||||
|
| `medium` | sehr gut | mittel | ~2–3 GB | |
|
||||||
|
| `large-v3` | ausgezeichnet | langsamer | ~4 GB | höchste Genauigkeit bei langen Texten |
|
||||||
|
| **`large-v3-turbo`** | ausgezeichnet | **schnell** | **~2,4 GB** | **Standard mit GPU** |
|
||||||
|
|
||||||
|
Gemessen wurde in diesem Projekt nur `large-v3-turbo` (RTX 3090); `small` auf CPU ist durch die Tests
|
||||||
|
erprobt. Die übrigen Angaben sind Richtwerte. Jedes Modell wird beim ersten Gebrauch einmalig heruntergeladen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Tipps für gute Erkennung
|
||||||
|
|
||||||
|
- **Abstand:** 30 cm bis 1 m zum Mikrofon, deutlich, aber normal sprechen.
|
||||||
|
- **Pause am Satzende:** Nach dem Satz kurz innehalten — das signalisiert „fertig“.
|
||||||
|
- **Denkpausen:** Wird ein Satz mitten drin abgeschnitten, `--silence-ms 1200` (oder mehr) verwenden.
|
||||||
|
- **Eigennamen:** Werden Namen falsch geschrieben, mit `--prompt "Name1, Name2"` vorgeben.
|
||||||
|
- **Hintergrundgeräusche:** Radio/Fernseher leiser; bei vielen Fehlauslösungen `STT_VAD_THRESHOLD=0.6`.
|
||||||
|
- **Sprache fest einstellen:** `-l de` ist schneller und zuverlässiger als `auto`, besonders bei kurzen
|
||||||
|
Sätzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Fehlerbehebung
|
||||||
|
|
||||||
|
| Problem | Ursache | Lösung |
|
||||||
|
|---|---|---|
|
||||||
|
| `Fehler: … Connection refused` / „Läuft der Dienst?“ | Dienst nicht gestartet | Dienst starten ([Abschnitt 4](#4-dienst-starten-und-prüfen)) oder `--local` nutzen |
|
||||||
|
| `/health` zeigt lange `loading` | erster Start: Modell wird heruntergeladen | Internetverbindung prüfen, warten; Fortschritt im Log |
|
||||||
|
| `/health` zeigt `"device":"cpu"`, obwohl eine GPU da ist | GPU belegt/zu wenig Speicher/CUDA-Problem | Log lesen (`Laden auf cuda fehlgeschlagen: …`), `nvidia-smi` prüfen, andere Karte per `CUDA_VISIBLE_DEVICES` |
|
||||||
|
| `/health` zeigt `error` | Modell nicht ladbar | Meldung im Feld `error`; Modellname und Speicherplatz prüfen |
|
||||||
|
| Nie „Sprache erkannt“ | falsches/stummes Mikrofon | [Abschnitt 6](#6-das-richtige-mikrofon); Pegel in den Toneinstellungen prüfen |
|
||||||
|
| Sätze werden mittendrin geteilt | Pausen zu lang für die Einstellung | `--silence-ms 1200` |
|
||||||
|
| Text kommt erst sehr spät | Pausenerkennung sehr lang eingestellt oder CPU-Betrieb | `--silence-ms` verringern; GPU nutzen |
|
||||||
|
| Wörter/Namen falsch | Aussprache, unbekannte Namen | `--prompt` mit Namen; größeres Modell; deutlicher sprechen |
|
||||||
|
| „Untertitel im Auftrag des ZDF“ o. Ä. | Whisper-Halluzination bei Stille | wird normalerweise gefiltert; tritt es trotzdem auf, bitte Beispiel melden |
|
||||||
|
| Wiederholte gleiche Sätze fehlen teilweise | Whisper fasst wörtliche Wiederholungen zusammen | bekannte Grenze; bei natürlicher Sprache selten |
|
||||||
|
| `Warteschlange voll` / HTTP 503 | zu viele Aufträge gleichzeitig | kurz warten; `STT_MAX_QUEUE` erhöhen |
|
||||||
|
| `Address already in use` beim Start | Port 9998/10300 belegt (zweiter Dienst läuft) | anderen Dienst beenden (`systemctl --user stop stt-service`) |
|
||||||
|
| Home Assistant findet den Dienst nicht | Firewall, falsche IP, Dienst aus | `curl http://<IP>:9998/health` vom HA-Rechner; [Abschnitt 8](#8-andere-geräte-im-netzwerk) |
|
||||||
|
| `Warnung: … Audioblöcke verworfen` | Netzwerk/Dienst zu langsam für das Mikrofon | Verbindung prüfen; lokal testen |
|
||||||
|
|
||||||
|
**Log ansehen:** manuell gestartet → im Terminal; als systemd-Dienst →
|
||||||
|
`journalctl --user -u stt-service -n 100`. Mehr Details mit `STT_LOG_LEVEL=DEBUG`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Datenschutz
|
||||||
|
|
||||||
|
- Die gesamte Spracherkennung läuft **lokal**. Audio und Text verlassen den Rechner nicht.
|
||||||
|
- Internetzugriff nur beim **ersten Herunterladen** eines Modells (Hugging Face).
|
||||||
|
- Der Dienst **speichert keine Aufnahmen** und keine Texte auf der Festplatte.
|
||||||
|
- **Ausnahme Log:** Bei Home-Assistant-Anfragen (Wyoming) wird der erkannte Text im Log protokolliert
|
||||||
|
(Stufe `INFO`). Wer das nicht möchte: `STT_LOG_LEVEL=WARNING`.
|
||||||
|
- Der Dienst hat keine Zugangskontrolle → nur im eigenen Netz betreiben ([Abschnitt 8](#8-andere-geräte-im-netzwerk)).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Deinstallation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --user disable --now stt-service 2>/dev/null
|
||||||
|
rm -f ~/.config/systemd/user/stt-service.service && systemctl --user daemon-reload
|
||||||
|
rm -rf ~/Python_Programs/SpeechToText
|
||||||
|
```
|
||||||
|
|
||||||
|
Heruntergeladene Modelle liegen im Hugging-Face-Cache (`$HF_HOME/hub` bzw. `~/.cache/huggingface/hub`,
|
||||||
|
Ordner `models--*faster-whisper*`) und werden eventuell auch von anderen Programmen genutzt — nur löschen,
|
||||||
|
wenn sicher ist, dass sie nicht mehr gebraucht werden.
|
||||||
|
|
@ -12,6 +12,13 @@ Lokale, schnelle, zuverlässige Spracherkennung, nicht blockierend. Baustein fü
|
||||||
Bewährtes STT-Vorbild: `~/my_voice_assistant_v3` (`app/providers/stt/faster_whisper.py`, `app/api/ws.py`).
|
Bewährtes STT-Vorbild: `~/my_voice_assistant_v3` (`app/providers/stt/faster_whisper.py`, `app/api/ws.py`).
|
||||||
Git-Remote auf kitux.de (Forgejo) geplant, noch nicht eingerichtet.
|
Git-Remote auf kitux.de (Forgejo) geplant, noch nicht eingerichtet.
|
||||||
|
|
||||||
|
## Dokumentation
|
||||||
|
|
||||||
|
`README.md` (Überblick) · `BEDIENUNGSANLEITUNG.md` (Anwender, vollständige Tabelle der `STT_*`-Variablen) ·
|
||||||
|
`docs/API.md` (HTTP/WebSocket/Wyoming-Protokoll) · `docs/ENTWICKLER.md` (Architektur, Nebenläufigkeitsregeln,
|
||||||
|
Tests, Erweitern). Bei Änderungen an Schnittstellen, Optionen oder Umgebungsvariablen die betroffenen Dokumente
|
||||||
|
mitpflegen.
|
||||||
|
|
||||||
## Befehle
|
## Befehle
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
155
README.md
155
README.md
|
|
@ -4,124 +4,87 @@ Lokaler, schneller Spracherkennungs-Dienst (Sprache → Text) auf Basis von
|
||||||
[faster-whisper](https://github.com/SYSTRAN/faster-whisper) — das STT-Gegenstück zu
|
[faster-whisper](https://github.com/SYSTRAN/faster-whisper) — das STT-Gegenstück zu
|
||||||
[chatterbox-tts-cli](https://kitux.de/forgejo/dschlueter/chatterbox-tts-cli).
|
[chatterbox-tts-cli](https://kitux.de/forgejo/dschlueter/chatterbox-tts-cli).
|
||||||
|
|
||||||
Gedacht als Baustein für Sprachassistenten und Smarthome: Home Assistant (Wyoming),
|
Gedacht als Baustein für Sprachassistenten und Smarthome: **Home Assistant** (Wyoming),
|
||||||
Mikrocontroller wie ESP32 (HTTP/WebSocket), Skripte (CLI) und OpenAI-kompatible Tools.
|
**Mikrocontroller** wie ESP32 (HTTP/WebSocket), **Skripte** (CLI) und **OpenAI-kompatible Tools**.
|
||||||
|
|
||||||
- **Schnell:** Modell bleibt geladen. Gemessen auf RTX 3090 mit `large-v3-turbo`:
|
- **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.
|
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).
|
- **Nicht blockierend:** Inferenz in einem Worker-Thread, begrenzte Warteschlange (503 statt Stau).
|
||||||
- **Äußerungsende per VAD** (Silero) statt Stoppwort; Transkript ca. 1 s nach Sprechende.
|
- **Äußerungsende per Sprechpause** (Silero-VAD) statt Stoppwort; Text ca. 1 s nach Sprechende.
|
||||||
- **Robust:** GPU-Fehler → automatischer CPU-Fallback; beliebige Audioformate; Halluzinationsfilter
|
- **Robust:** automatischer CPU-Fallback, beliebige Audioformate, Filter gegen Whisper-Halluzinationen.
|
||||||
(„Untertitel im Auftrag des ZDF“ …).
|
- **Lokal:** keine Cloud; Internet nur zum einmaligen Herunterladen des Modells.
|
||||||
|
|
||||||
## Installation
|
## Dokumentation
|
||||||
|
|
||||||
```bash
|
| Dokument | Für wen | Inhalt |
|
||||||
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) |
|
| **[BEDIENUNGSANLEITUNG.md](BEDIENUNGSANLEITUNG.md)** | Anwender | Installation, Starten, CLI, Mikrofon, Home Assistant, Einstellungen, Fehlerbehebung |
|
||||||
| `STT_DEVICE` | `auto` | `auto` / `cuda` / `cpu` |
|
| **[docs/API.md](docs/API.md)** | Client-Entwickler | HTTP, WebSocket-Protokoll, Wyoming, Fehlercodes, Beispiele (Python, curl, ESP32) |
|
||||||
| `STT_COMPUTE_TYPE` | `auto` | `float16` (GPU) / `int8` (CPU) automatisch |
|
| **[docs/ENTWICKLER.md](docs/ENTWICKLER.md)** | Programmierer am Projekt | Architektur, Module, Nebenläufigkeit, Tests, Erweitern, Designentscheidungen |
|
||||||
| `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
|
## Schnellstart
|
||||||
|
|
||||||
### HTTP
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# OpenAI-kompatibel (multipart) — response_format: json | text | verbose_json
|
cd ~/Python_Programs/SpeechToText
|
||||||
curl -F file=@aufnahme.mp3 -F language=de http://127.0.0.1:9998/v1/audio/transcriptions
|
python3 -m venv --system-site-packages .venv
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
# Rohdaten im Body — ideal für Mikrocontroller (kein multipart nötig)
|
# Dienst starten (GPU per UUID aus `nvidia-smi -L`; ohne GPU einfach weglassen)
|
||||||
|
CUDA_VISIBLE_DEVICES=GPU-<uuid> .venv/bin/python stt_service.py
|
||||||
|
|
||||||
|
# in einem zweiten Terminal
|
||||||
|
curl http://127.0.0.1:9998/health # "status":"ok" = bereit
|
||||||
|
.venv/bin/python stt_cli.py aufnahme.wav # Datei → Text
|
||||||
|
.venv/bin/python stt_cli.py -v # Mikrofon → Text (Strg+C beendet)
|
||||||
|
```
|
||||||
|
|
||||||
|
Beim ersten Start wird das Modell geladen (`large-v3-turbo` ≈ 1,6 GB). Schnittstellen zum Ausprobieren
|
||||||
|
im Browser: http://127.0.0.1:9998/docs
|
||||||
|
|
||||||
|
## Schnittstellen auf einen Blick
|
||||||
|
|
||||||
|
| Schnittstelle | Adresse | Zweck |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET /health` | `:9998` | Status, Modell, Gerät, Warteschlange |
|
||||||
|
| `POST /v1/audio/transcriptions` | `:9998` | OpenAI-kompatibel (multipart) |
|
||||||
|
| `POST /transcribe` | `:9998` | Audio direkt im Body (WAV/MP3/… oder rohes PCM) — ideal für Mikrocontroller |
|
||||||
|
| `WS /ws/stream` | `:9998` | Live-Streaming mit automatischer Satzende-Erkennung |
|
||||||
|
| Wyoming | `tcp :10300` | Home Assistant / ESPHome-Satelliten |
|
||||||
|
|
||||||
|
```bash
|
||||||
curl --data-binary @aufnahme.wav http://127.0.0.1:9998/transcribe
|
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"
|
curl -F file=@aufnahme.mp3 http://127.0.0.1:9998/v1/audio/transcriptions
|
||||||
```
|
```
|
||||||
|
|
||||||
`/transcribe` liefert `text`, `language`, `duration`, `processing_s`, `segments`.
|
> Der Dienst hat keine Authentifizierung — nur im eigenen Netz betreiben.
|
||||||
|
|
||||||
### WebSocket `/ws/stream` (Live-Streaming)
|
## Projektaufbau
|
||||||
|
|
||||||
Client schickt s16le-PCM in beliebig großen Stücken; der Dienst erkennt Äußerungen selbst.
|
| Datei | Aufgabe |
|
||||||
|
|---|---|
|
||||||
```text
|
| `stt_core.py` | Kern: Whisper-Engine, Audio-Hilfen, Halluzinationsfilter, Satzende-Erkennung |
|
||||||
→ {"type":"start","sample_rate":16000,"channels":1,"channel":null,"language":"de","vad":true,"silence_ms":700}
|
| `stt_service.py` | FastAPI-Dienst: HTTP, WebSocket, Worker, Start des Wyoming-Servers |
|
||||||
→ <binär: PCM> …
|
| `stt_wyoming.py` | Wyoming-Protokoll für Home Assistant |
|
||||||
← {"type":"speech_start","t":1.31}
|
| `stt_cli.py` | Kommandozeile: Dateien und Mikrofon über den Dienst oder lokal |
|
||||||
← {"type":"transcript","text":"Schalte das Licht ein.","language":"de","duration":2.4,"processing_s":0.21,"reason":"silence","t":3.9}
|
| `tests/` | Unit-Tests (ohne Modell) und Integrationstests (echtes Whisper) |
|
||||||
→ {"type":"end"} # laufende Äußerung abschließen → danach {"type":"done"}
|
| `deploy/stt-service.service` | systemd-User-Unit für den Dauerbetrieb |
|
||||||
→ {"type":"close"} # ausstehende Transkripte senden, dann schließen
|
| `legacy/` | alte Einzelskripte `stt.py`, `stt_respeaker.py` (historisch, nicht gepflegt) |
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.venv/bin/python -m pytest # schnell, ohne Modell (Fake-Engine)
|
.venv/bin/python -m pytest # 36 Unit-Tests, ohne Modell
|
||||||
.venv/bin/python -m pytest -m integration # echtes Whisper (small, CPU) + Silero mit espeak-ng-Sprache
|
.venv/bin/python -m pytest -m integration # 4 Integrationstests: Whisper small (CPU) + Silero, espeak-ng-Sprache
|
||||||
.venv/bin/python -m pytest tests/test_core.py::test_segmenter_detects_two_utterances
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bekannte Grenzen
|
## Bekannte Grenzen
|
||||||
|
|
||||||
- Wörtlich wiederholte Sätze innerhalb eines 30-s-Fensters fasst Whisper teils zusammen
|
- Wörtlich wiederholte Sätze innerhalb eines 30-s-Fensters fasst Whisper teils zusammen
|
||||||
(bei natürlicher Sprache selten).
|
(bei natürlicher Sprache selten).
|
||||||
- Keine wortweisen Zwischenergebnisse während des Sprechens — Transkript kommt pro Äußerung.
|
- Keine Zwischenergebnisse während des Sprechens — Text kommt pro Äußerung.
|
||||||
- `legacy/stt.py` und `legacy/stt_respeaker.py` sind die alten Einzelskripte (historisch, nicht gepflegt).
|
- Keine Authentifizierung und kein TLS.
|
||||||
|
|
||||||
|
## Lizenz und Autor
|
||||||
|
|
||||||
|
Autor: dschlueter. Verwendete Komponenten: faster-whisper (MIT), OpenAI Whisper-Modelle (MIT),
|
||||||
|
Silero VAD (MIT), Wyoming (MIT), FastAPI (MIT).
|
||||||
|
|
|
||||||
384
docs/API.md
Normal file
384
docs/API.md
Normal file
|
|
@ -0,0 +1,384 @@
|
||||||
|
# API-Referenz
|
||||||
|
|
||||||
|
Schnittstellen des STT-Dienstes (`stt_service.py`) für Client-Entwickler — Skripte, Web-Apps,
|
||||||
|
Mikrocontroller (ESP32), Home Assistant.
|
||||||
|
|
||||||
|
| Schnittstelle | Adresse | Gedacht für |
|
||||||
|
|---|---|---|
|
||||||
|
| [`GET /health`](#get-health) | `http://<host>:9998/health` | Überwachung, Bereitschaft prüfen |
|
||||||
|
| [`POST /v1/audio/transcriptions`](#post-v1audiotranscriptions-openai-kompatibel) | multipart | Tools mit OpenAI-Client, Web-Upload |
|
||||||
|
| [`POST /transcribe`](#post-transcribe-rohdaten) | Rohdaten im Body | Mikrocontroller, `curl`, einfache Skripte |
|
||||||
|
| [`WS /ws/stream`](#ws-wsstream-live-streaming) | WebSocket | Live-Mikrofon, fortlaufendes Diktat, Push-to-Talk |
|
||||||
|
| [Wyoming](#wyoming-home-assistant) | `tcp://<host>:10300` | Home Assistant, ESPHome-Satelliten |
|
||||||
|
|
||||||
|
Interaktive Doku (Swagger) mit „Try it out“: **`http://<host>:9998/docs`**
|
||||||
|
|
||||||
|
> **Sicherheit:** Der Dienst hat **keine Authentifizierung** und lauscht auf allen Netzwerkschnittstellen
|
||||||
|
> (`0.0.0.0`). Nur im vertrauenswürdigen LAN betreiben bzw. per Firewall absichern
|
||||||
|
> (z. B. `sudo ufw allow from 192.168.1.0/24 to any port 9998,10300 proto tcp`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gemeinsame Konzepte
|
||||||
|
|
||||||
|
### Audio
|
||||||
|
|
||||||
|
- Intern arbeitet Whisper mit **16 kHz, mono, float32**. Alles andere wird vom Dienst umgerechnet.
|
||||||
|
- Container-Formate (`/v1/audio/transcriptions`, `/transcribe` ohne `format=pcm`): WAV (jede Bittiefe und
|
||||||
|
Kanalzahl), MP3, OGG/Opus, FLAC, M4A/AAC, WebM … — alles, was FFmpeg/PyAV lesen kann.
|
||||||
|
- Rohdaten (`/transcribe?format=pcm`, WebSocket, Wyoming): **PCM signed 16 bit little-endian** (`s16le`),
|
||||||
|
bei mehreren Kanälen verschachtelt (L R L R …).
|
||||||
|
- Empfehlung für Mikrocontroller: direkt **16 kHz mono s16le** senden — dann entfällt jede Umrechnung.
|
||||||
|
|
||||||
|
### Sprache (`language`)
|
||||||
|
|
||||||
|
| Wert | Wirkung |
|
||||||
|
|---|---|
|
||||||
|
| weggelassen | Default des Dienstes (`STT_LANGUAGE`, Standard `de`) |
|
||||||
|
| `de`, `en`, `fr`, … | feste Sprache (ISO 639-1); `de-DE` wird zu `de` |
|
||||||
|
| `auto` | automatische Spracherkennung (etwas langsamer, bei kurzen Äußerungen unsicherer) |
|
||||||
|
|
||||||
|
### Prompt (`prompt`)
|
||||||
|
|
||||||
|
Optionaler Kontext für Whisper, v. a. **Eigennamen und Fachbegriffe**, z. B.
|
||||||
|
`"Bredelar, Marsberg, Hoppermann"`. Verbessert die Schreibweise solcher Wörter.
|
||||||
|
|
||||||
|
### Transkript-Objekt
|
||||||
|
|
||||||
|
Von `/transcribe` (vollständig) und WebSocket (ohne `segments`) geliefert:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Schalte das Licht im Wohnzimmer ein.",
|
||||||
|
"language": "de",
|
||||||
|
"language_probability": 1.0,
|
||||||
|
"duration": 2.41,
|
||||||
|
"processing_s": 0.198,
|
||||||
|
"segments": [
|
||||||
|
{"start": 0.0, "end": 2.3, "text": "Schalte das Licht im Wohnzimmer ein.",
|
||||||
|
"avg_logprob": -0.21, "no_speech_prob": 0.01}
|
||||||
|
],
|
||||||
|
"dropped_segments": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Feld | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `text` | Ergebnis; **leerer String**, wenn keine Sprache erkannt wurde |
|
||||||
|
| `language` / `language_probability` | erkannte bzw. vorgegebene Sprache und Sicherheit |
|
||||||
|
| `duration` | Audiodauer in Sekunden |
|
||||||
|
| `processing_s` | Rechenzeit der Transkription in Sekunden |
|
||||||
|
| `segments` | Satzabschnitte mit Zeitstempeln und Whisper-Konfidenzwerten |
|
||||||
|
| `dropped_segments` | vom Halluzinationsfilter verworfene Abschnitte (z. B. „Untertitel im Auftrag des ZDF“) |
|
||||||
|
|
||||||
|
### Fehler
|
||||||
|
|
||||||
|
HTTP-Fehler kommen als JSON `{"detail": "…"}`:
|
||||||
|
|
||||||
|
| Status | Ursache | Client-Reaktion |
|
||||||
|
|---|---|---|
|
||||||
|
| `400` | Audio nicht dekodierbar, leerer Body, ungültiger Kanal, unbekanntes `response_format` | Eingabe prüfen |
|
||||||
|
| `413` | Upload größer als `STT_MAX_UPLOAD_MB` (Standard 50 MB) | Datei kürzen/teilen |
|
||||||
|
| `422` | Parameter außerhalb des gültigen Bereichs (z. B. `sample_rate=100`) | Parameter prüfen |
|
||||||
|
| `500` | Modell nicht ladbar / interner Fehler | `/health` und Log prüfen |
|
||||||
|
| `503` | Warteschlange voll (`STT_MAX_QUEUE`), Header `Retry-After: 2` | nach 2 s erneut versuchen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `GET /health`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://127.0.0.1:9998/health
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"model": "large-v3-turbo",
|
||||||
|
"device": "cuda",
|
||||||
|
"compute_type": "float16",
|
||||||
|
"default_language": "de",
|
||||||
|
"queue": 0,
|
||||||
|
"max_queue": 8,
|
||||||
|
"load_seconds": 2.34,
|
||||||
|
"wyoming_port": 10300,
|
||||||
|
"error": null,
|
||||||
|
"version": "0.1.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| `status` | Bedeutung |
|
||||||
|
|---|---|
|
||||||
|
| `ok` | Modell geladen, Dienst bereit |
|
||||||
|
| `loading` | Modell wird gerade geladen (erste Anfragen warten automatisch) |
|
||||||
|
| `idle` | Modell noch nicht geladen (nur bei `STT_PRELOAD=0`; lädt bei der ersten Anfrage) |
|
||||||
|
| `error` | Modell nicht ladbar — Grund in `error` |
|
||||||
|
|
||||||
|
`device` = `cpu` bei laufender GPU bedeutet: GPU-Laden ist fehlgeschlagen, der Dienst ist auf CPU
|
||||||
|
ausgewichen (langsamer, Modell `STT_CPU_MODEL`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `POST /v1/audio/transcriptions` (OpenAI-kompatibel)
|
||||||
|
|
||||||
|
Nachbildung der [OpenAI-Transcriptions-API](https://platform.openai.com/docs/api-reference/audio/createTranscription).
|
||||||
|
Bestehende OpenAI-Clients funktionieren, wenn man die Basis-URL auf `http://<host>:9998/v1` setzt.
|
||||||
|
|
||||||
|
**Form-Felder (multipart/form-data):**
|
||||||
|
|
||||||
|
| Feld | Pflicht | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `file` | ja | Audiodatei |
|
||||||
|
| `language` | nein | siehe [Sprache](#sprache-language) |
|
||||||
|
| `prompt` | nein | siehe [Prompt](#prompt-prompt) |
|
||||||
|
| `response_format` | nein | `json` (Standard) · `text` · `verbose_json` |
|
||||||
|
| `model`, `temperature` | nein | werden akzeptiert, aber ignoriert (ein Modell pro Dienst) |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -F file=@aufnahme.mp3 -F language=de http://127.0.0.1:9998/v1/audio/transcriptions
|
||||||
|
# {"text":"…"}
|
||||||
|
|
||||||
|
curl -F file=@aufnahme.mp3 -F response_format=text http://127.0.0.1:9998/v1/audio/transcriptions
|
||||||
|
# …reiner Text…
|
||||||
|
```
|
||||||
|
|
||||||
|
`verbose_json` liefert `task`, `language`, `duration`, `text` und `segments` (mit `id`, `start`, `end`,
|
||||||
|
`text`, `avg_logprob`, `no_speech_prob`).
|
||||||
|
|
||||||
|
**Python mit dem offiziellen OpenAI-Paket:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
client = OpenAI(base_url="http://127.0.0.1:9998/v1", api_key="unbenutzt")
|
||||||
|
with open("aufnahme.mp3", "rb") as fh:
|
||||||
|
result = client.audio.transcriptions.create(model="whisper-1", file=fh, language="de")
|
||||||
|
print(result.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `POST /transcribe` (Rohdaten)
|
||||||
|
|
||||||
|
Audio steht **direkt im Request-Body** — kein multipart, ideal für Mikrocontroller und `curl`.
|
||||||
|
|
||||||
|
**Query-Parameter:**
|
||||||
|
|
||||||
|
| Parameter | Standard | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `format` | `auto` | `auto` = Container (WAV, MP3 …) erkennen · `pcm` = rohes s16le |
|
||||||
|
| `sample_rate` | `16000` | nur `pcm`: Abtastrate (4000–192000) |
|
||||||
|
| `channels` | `1` | nur `pcm`: Kanalzahl (1–16) |
|
||||||
|
| `channel` | – | nur `pcm`: einen Kanal auswählen (0-basiert), sonst Mittelwert aller Kanäle |
|
||||||
|
| `language` | – | siehe [Sprache](#sprache-language) |
|
||||||
|
| `prompt` | – | siehe [Prompt](#prompt-prompt) |
|
||||||
|
|
||||||
|
Antwort: vollständiges [Transkript-Objekt](#transkript-objekt).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WAV/MP3 direkt
|
||||||
|
curl --data-binary @aufnahme.wav http://127.0.0.1:9998/transcribe
|
||||||
|
|
||||||
|
# Rohes PCM erzeugen und senden (16 kHz mono)
|
||||||
|
ffmpeg -i aufnahme.mp3 -f s16le -ar 16000 -ac 1 roh.pcm
|
||||||
|
curl --data-binary @roh.pcm "http://127.0.0.1:9998/transcribe?format=pcm&sample_rate=16000"
|
||||||
|
|
||||||
|
# ReSpeaker-Aufnahme: 48 kHz Stereo, nur rechter ASR-Kanal
|
||||||
|
curl --data-binary @respeaker.pcm \
|
||||||
|
"http://127.0.0.1:9998/transcribe?format=pcm&sample_rate=48000&channels=2&channel=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python (requests):**
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
with open("aufnahme.wav", "rb") as fh:
|
||||||
|
r = requests.post("http://127.0.0.1:9998/transcribe", data=fh, params={"language": "de"}, timeout=120)
|
||||||
|
r.raise_for_status()
|
||||||
|
print(r.json()["text"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**ESP32 (Arduino, Skizze — nicht getestet):** Eine mit I²S aufgenommene Äußerung als PCM hochladen.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <HTTPClient.h>
|
||||||
|
|
||||||
|
// pcm: int16_t-Puffer, 16 kHz mono, samples = Anzahl Samples
|
||||||
|
String transcribe(const int16_t* pcm, size_t samples) {
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin("http://192.168.1.10:9998/transcribe?format=pcm&sample_rate=16000&language=de");
|
||||||
|
http.addHeader("Content-Type", "application/octet-stream");
|
||||||
|
http.setTimeout(15000);
|
||||||
|
int code = http.POST((uint8_t*)pcm, samples * sizeof(int16_t));
|
||||||
|
String body = code == 200 ? http.getString() : String();
|
||||||
|
http.end();
|
||||||
|
return body; // JSON; Feld "text" z. B. mit ArduinoJson auslesen
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Speicherbedarf: 1 s Audio = 32 KB. Für längere Aufnahmen den [WebSocket](#ws-wsstream-live-streaming) nutzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `WS /ws/stream` (Live-Streaming)
|
||||||
|
|
||||||
|
Der Client schickt fortlaufend Audio, der Dienst **erkennt Äußerungen selbst** (Silero-VAD: Sprache →
|
||||||
|
Stille ≥ `silence_ms`) und schickt pro Äußerung ein Transkript. Die Verbindung bleibt für beliebig viele
|
||||||
|
Äußerungen offen. Äußerungen werden der Reihe nach transkribiert; der Empfang läuft währenddessen weiter.
|
||||||
|
|
||||||
|
### Ablauf
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant S as STT-Dienst
|
||||||
|
C->>S: WebSocket verbinden
|
||||||
|
S-->>C: {"type":"ready"}
|
||||||
|
C->>S: {"type":"start", "sample_rate":48000, ...} (optional)
|
||||||
|
S-->>C: {"type":"ready"}
|
||||||
|
loop fortlaufend
|
||||||
|
C->>S: Binär: PCM s16le
|
||||||
|
end
|
||||||
|
S-->>C: {"type":"speech_start","t":1.31}
|
||||||
|
Note over S: Stille ≥ silence_ms → Äußerung fertig
|
||||||
|
S-->>C: {"type":"transcript","text":"…","reason":"silence"}
|
||||||
|
C->>S: {"type":"end"}
|
||||||
|
S-->>C: {"type":"transcript", "reason":"flush"} (falls noch Sprache lief)
|
||||||
|
S-->>C: {"type":"done"}
|
||||||
|
C->>S: {"type":"close"}
|
||||||
|
S-->>C: Verbindung geschlossen
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client → Dienst
|
||||||
|
|
||||||
|
**Binär-Frames:** PCM s16le in beliebig großen Stücken (empfohlen 20–100 ms). Kanalzahl und Abtastrate
|
||||||
|
gemäß `start`-Frame (Standard 16 kHz mono).
|
||||||
|
|
||||||
|
**`start`** (optional, jederzeit; setzt die Sitzung zurück):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type": "start", "sample_rate": 16000, "channels": 1, "channel": null,
|
||||||
|
"language": "de", "prompt": null, "vad": true,
|
||||||
|
"silence_ms": 700, "threshold": 0.5, "min_speech_ms": 200, "pre_roll_ms": 300, "max_utterance_s": 30}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Feld | Standard | Beschreibung |
|
||||||
|
|---|---|---|
|
||||||
|
| `sample_rate` | `16000` | Abtastrate der Binär-Frames |
|
||||||
|
| `channels` | `1` | Kanalzahl der Binär-Frames |
|
||||||
|
| `channel` | `null` | einen Kanal auswählen (ReSpeaker XVF3800: `1`), sonst Mittelwert |
|
||||||
|
| `language`, `prompt` | – | wie bei HTTP |
|
||||||
|
| `vad` | `true` | `true` = Äußerungen automatisch erkennen · `false` = sammeln bis `end` (Push-to-Talk) |
|
||||||
|
| `silence_ms` | `700` | Stille, die eine Äußerung beendet (Diktat mit Denkpausen: 1200–2000) |
|
||||||
|
| `threshold` | `0.5` | Silero-Sprachwahrscheinlichkeit (höher = unempfindlicher gegen Geräusche) |
|
||||||
|
| `min_speech_ms` | `200` | Mindestdauer Sprache, bevor eine Äußerung beginnt (filtert Klicks) |
|
||||||
|
| `pre_roll_ms` | `300` | Audio vor dem Sprachbeginn mitnehmen (verhindert abgeschnittene Anlaute) |
|
||||||
|
| `max_utterance_s` | `30` | harte Obergrenze; längere Äußerungen werden geteilt (`reason: max_length`) |
|
||||||
|
|
||||||
|
Nicht angegebene VAD-Werte kommen aus der Dienst-Konfiguration (`STT_VAD_*`).
|
||||||
|
|
||||||
|
**`end`** — laufende Äußerung sofort abschließen (mit VAD) bzw. alles bisher Gesendete transkribieren
|
||||||
|
(ohne VAD). Nach allen daraus entstehenden Transkripten folgt `done`. Die Verbindung bleibt offen.
|
||||||
|
|
||||||
|
**`close`** — wie `end`, danach schließt der Dienst die Verbindung, sobald ausstehende Transkripte
|
||||||
|
gesendet sind.
|
||||||
|
|
||||||
|
### Dienst → Client
|
||||||
|
|
||||||
|
| `type` | Felder | Bedeutung |
|
||||||
|
|---|---|---|
|
||||||
|
| `ready` | `sample_rate`, `vad` | nach Verbindungsaufbau und nach jedem `start` |
|
||||||
|
| `speech_start` | `t` | Sprachbeginn erkannt (Stream-Zeit in s) — z. B. für eine „hört zu“-LED |
|
||||||
|
| `transcript` | Transkript-Objekt ohne `segments`, plus `reason`, `t` | Ergebnis einer Äußerung; `text` kann leer sein (nur Geräusch) |
|
||||||
|
| `done` | – | alle durch `end` ausgelösten Transkripte sind gesendet |
|
||||||
|
| `error` | `detail` | z. B. ungültiger Frame, Warteschlange voll; Verbindung bleibt bestehen |
|
||||||
|
|
||||||
|
`reason`: `silence` (Stille erkannt) · `max_length` (Obergrenze erreicht) · `flush` (durch `end`/`close`).
|
||||||
|
|
||||||
|
### Beispiel: Python-Client (Datei in Echtzeit streamen)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json, threading, time, wave
|
||||||
|
from websockets.sync.client import connect
|
||||||
|
|
||||||
|
with wave.open("aufnahme.wav", "rb") as wav, connect("ws://127.0.0.1:9998/ws/stream") as ws:
|
||||||
|
rate, channels = wav.getframerate(), wav.getnchannels() # WAV muss 16 Bit sein
|
||||||
|
ws.recv() # ready
|
||||||
|
ws.send(json.dumps({"type": "start", "sample_rate": rate, "channels": channels}))
|
||||||
|
ws.recv() # ready
|
||||||
|
|
||||||
|
def receive():
|
||||||
|
for raw in ws:
|
||||||
|
event = json.loads(raw)
|
||||||
|
if event["type"] == "transcript" and event["text"]:
|
||||||
|
print(event["text"])
|
||||||
|
if event["type"] == "done":
|
||||||
|
return
|
||||||
|
|
||||||
|
receiver = threading.Thread(target=receive)
|
||||||
|
receiver.start()
|
||||||
|
block = rate // 10 # 100 ms
|
||||||
|
while chunk := wav.readframes(block):
|
||||||
|
ws.send(chunk)
|
||||||
|
time.sleep(0.1) # Echtzeit simulieren
|
||||||
|
ws.send(json.dumps({"type": "end"}))
|
||||||
|
receiver.join()
|
||||||
|
```
|
||||||
|
|
||||||
|
Ein vollständiger Mikrofon-Client ist `stt_cli.py` (Funktion `service_mic`).
|
||||||
|
|
||||||
|
### Hinweise
|
||||||
|
|
||||||
|
- Der Sprachbeginn wird nach ca. `min_speech_ms`, das Ende nach `silence_ms` erkannt; das Transkript
|
||||||
|
kommt typischerweise **~1 s nach Sprechende** (700 ms Stille + ~0,2 s Rechenzeit auf GPU).
|
||||||
|
- Wer schneller senden kann als Echtzeit (z. B. Datei), darf das — die Äußerungserkennung arbeitet auf
|
||||||
|
der Stream-Zeit, nicht auf der Uhrzeit.
|
||||||
|
- Pro Verbindung eine eigene Sitzung; mehrere Verbindungen teilen sich den einen Worker (Reihenfolge
|
||||||
|
über alle Verbindungen, Warteschlangenlimit gilt gesamt).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wyoming (Home Assistant)
|
||||||
|
|
||||||
|
TCP-Server nach dem [Wyoming-Protokoll](https://github.com/OHF-Voice/wyoming) auf Port **10300**
|
||||||
|
(`STT_WYOMING_PORT`, leer = deaktiviert).
|
||||||
|
|
||||||
|
**Einrichten in Home Assistant:** Einstellungen → Geräte & Dienste → Integration hinzufügen →
|
||||||
|
**Wyoming Protocol** → Host = IP dieses Rechners, Port `10300`. Anschließend unter Einstellungen →
|
||||||
|
Sprachassistenten die Pipeline öffnen und als **Spracherkennung** „speech-to-text“ wählen.
|
||||||
|
|
||||||
|
| Ereignis (HA → Dienst) | Verarbeitung |
|
||||||
|
|---|---|
|
||||||
|
| `describe` | Antwort `info`: Programm `speech-to-text`, Modellname, unterstützte Sprachen |
|
||||||
|
| `transcribe` | merkt `language`; `transcript_names`/`transcript_terms` werden als Whisper-`hotwords` genutzt |
|
||||||
|
| `audio-start` / `audio-chunk` | Audio sammeln; beliebige Rate/Bittiefe/Kanäle werden nach 16 kHz mono konvertiert |
|
||||||
|
| `audio-stop` | transkribieren → `transcript` (`text`, `language`), danach Verbindung schließen |
|
||||||
|
|
||||||
|
Das Äußerungsende bestimmt Home Assistant bzw. der Satellit (ESPHome) selbst. Bei einem Fehler wird
|
||||||
|
ein leeres Transkript gesendet, damit HA nicht hängen bleibt.
|
||||||
|
|
||||||
|
Test ohne Home Assistant (Python, Paket `wyoming`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio, wave
|
||||||
|
from wyoming.asr import Transcribe, Transcript
|
||||||
|
from wyoming.audio import AudioChunk, AudioStart, AudioStop
|
||||||
|
from wyoming.client import AsyncTcpClient
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
with wave.open("aufnahme.wav", "rb") as wav:
|
||||||
|
rate, width, ch = wav.getframerate(), wav.getsampwidth(), wav.getnchannels()
|
||||||
|
audio = wav.readframes(wav.getnframes())
|
||||||
|
async with AsyncTcpClient("127.0.0.1", 10300) as client:
|
||||||
|
await client.write_event(Transcribe(language="de").event())
|
||||||
|
await client.write_event(AudioStart(rate=rate, width=width, channels=ch).event())
|
||||||
|
step = rate * width * ch // 10
|
||||||
|
for i in range(0, len(audio), step):
|
||||||
|
await client.write_event(AudioChunk(rate=rate, width=width, channels=ch, audio=audio[i:i + step]).event())
|
||||||
|
await client.write_event(AudioStop().event())
|
||||||
|
print(Transcript.from_event(await client.read_event()).text)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
553
docs/ENTWICKLER.md
Normal file
553
docs/ENTWICKLER.md
Normal file
|
|
@ -0,0 +1,553 @@
|
||||||
|
# Entwicklerdokumentation
|
||||||
|
|
||||||
|
Für Programmierer, die den Code verstehen, ändern oder erweitern wollen. Anwender lesen die
|
||||||
|
[Bedienungsanleitung](../BEDIENUNGSANLEITUNG.md), Client-Entwickler die [API-Referenz](API.md).
|
||||||
|
|
||||||
|
## Inhalt
|
||||||
|
|
||||||
|
1. [Designziele](#1-designziele)
|
||||||
|
2. [Projektstruktur](#2-projektstruktur)
|
||||||
|
3. [Architektur im Überblick](#3-architektur-im-überblick)
|
||||||
|
4. [Modul `stt_core.py`](#4-modul-stt_corepy)
|
||||||
|
5. [Modul `stt_service.py`](#5-modul-stt_servicepy)
|
||||||
|
6. [Modul `stt_wyoming.py`](#6-modul-stt_wyomingpy)
|
||||||
|
7. [Modul `stt_cli.py`](#7-modul-stt_clipy)
|
||||||
|
8. [Nebenläufigkeit — die wichtigsten Regeln](#8-nebenläufigkeit--die-wichtigsten-regeln)
|
||||||
|
9. [Konfiguration](#9-konfiguration)
|
||||||
|
10. [Entwicklungsumgebung](#10-entwicklungsumgebung)
|
||||||
|
11. [Tests](#11-tests)
|
||||||
|
12. [Erweitern: typische Aufgaben](#12-erweitern-typische-aufgaben)
|
||||||
|
13. [Designentscheidungen](#13-designentscheidungen)
|
||||||
|
14. [Bekannte Grenzen und mögliche nächste Schritte](#14-bekannte-grenzen-und-mögliche-nächste-schritte)
|
||||||
|
15. [Konventionen](#15-konventionen)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Designziele
|
||||||
|
|
||||||
|
| Ziel | Umsetzung |
|
||||||
|
|---|---|
|
||||||
|
| **Schnell** | Modell einmal laden und halten; `large-v3-turbo` auf GPU; genau eine Transkription pro Äußerung |
|
||||||
|
| **Nicht blockierend** | Inferenz in einem Worker-Thread; Event-Loop bleibt frei; begrenzte Warteschlange mit sofortigem 503 |
|
||||||
|
| **Zuverlässig** | GPU→CPU-Fallback beim Laden; Halluzinationsfilter; robustes Dekodieren beliebiger Formate; Fehler einzelner Anfragen beenden keine Verbindung |
|
||||||
|
| **Vielseitig anbindbar** | eine Engine, mehrere Schnittstellen: OpenAI-HTTP, Roh-HTTP, WebSocket, Wyoming, CLI |
|
||||||
|
| **Testbar** | reine Logik ohne I/O im Kern; App-Fabrik mit injizierbarer Engine und VAD |
|
||||||
|
| **Gegenstück zu chatterbox-tts-cli** | gleiche Bauart: Kernmodul + FastAPI-Dienst + CLI + systemd-User-Unit |
|
||||||
|
|
||||||
|
Das Projekt ersetzt das alte Einzelskript `legacy/stt.py`. Dessen Schwächen — Stoppwort-Erkennung in
|
||||||
|
starren 2-s-Blöcken, doppelte Transkription, Modell-Laden pro Aufruf, falsches Lesen von Stereo-WAVs,
|
||||||
|
alles in einer Datei mit `print`/`sys.exit` — haben das Design direkt geprägt. Als Vorbild für
|
||||||
|
funktionierendes STT diente `my_voice_assistant_v3` (Modell-Cache, `asyncio.to_thread`, VAD-Segmentierung,
|
||||||
|
Warm-up).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
SpeechToText/
|
||||||
|
├── stt_core.py Kern: Engine, Audio-Hilfen, Halluzinationsfilter, VAD-Segmentierung (kein I/O)
|
||||||
|
├── stt_service.py FastAPI-Dienst: HTTP-Endpunkte, WebSocket, Worker, Start des Wyoming-Servers
|
||||||
|
├── stt_wyoming.py Wyoming-Protokoll-Handler (Home Assistant)
|
||||||
|
├── stt_cli.py Client: Dateien/Mikrofon → Dienst, oder --local direkt mit dem Kern
|
||||||
|
├── tests/
|
||||||
|
│ ├── conftest.py Fixtures und Test-Helfer (FakeEngine, EnergyProb, make_wav, tone, espeak_wav)
|
||||||
|
│ ├── test_core.py Audio, Filter, Konfiguration, Segmentierung
|
||||||
|
│ ├── test_service.py HTTP, Überlast, Nicht-Blockieren, WebSocket
|
||||||
|
│ ├── test_wyoming.py Wyoming über echten TCP-Socket
|
||||||
|
│ └── test_integration.py echtes Whisper + Silero (Marker "integration")
|
||||||
|
├── deploy/stt-service.service systemd-User-Unit
|
||||||
|
├── docs/API.md Schnittstellen-Referenz
|
||||||
|
├── docs/ENTWICKLER.md dieses Dokument
|
||||||
|
├── BEDIENUNGSANLEITUNG.md Anwender-Handbuch
|
||||||
|
├── README.md Überblick und Schnellstart
|
||||||
|
├── CLAUDE.md Kurzkontext für Claude Code
|
||||||
|
├── requirements.txt
|
||||||
|
├── pytest.ini Marker, Standard: Integrationstests ausgeschlossen
|
||||||
|
└── legacy/ historische Einzelskripte (nicht gepflegt)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Abhängigkeitsrichtung** (keine Zyklen):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
cli[stt_cli.py] -. "nur mit Option local" .-> core[stt_core.py]
|
||||||
|
service[stt_service.py] --> core
|
||||||
|
service -. Import zur Laufzeit .-> wyoming[stt_wyoming.py]
|
||||||
|
wyoming --> core
|
||||||
|
cli -- HTTP/WS --> service
|
||||||
|
```
|
||||||
|
|
||||||
|
`stt_core` importiert nichts aus dem Projekt. `stt_wyoming` wird erst beim Start importiert, damit der
|
||||||
|
Dienst ohne das Paket `wyoming` lauffähig bleibt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architektur im Überblick
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph Clients
|
||||||
|
HA[Home Assistant / ESPHome]
|
||||||
|
ESP[ESP32 / Skripte]
|
||||||
|
OAI[OpenAI-kompatible Tools]
|
||||||
|
CLI[stt_cli.py]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph Prozess["stt_service.py (ein Prozess, ein Event-Loop)"]
|
||||||
|
WY[Wyoming-Handler<br/>stt_wyoming.py]
|
||||||
|
HTTP[HTTP-Endpunkte<br/>/transcribe, /v1/audio/transcriptions]
|
||||||
|
WS[_StreamSession<br/>/ws/stream]
|
||||||
|
SEG[UtteranceSegmenter<br/>+ SileroProb]
|
||||||
|
W[TranscriptionWorker<br/>ThreadPoolExecutor 1 Thread<br/>pending ≤ max_queue]
|
||||||
|
E[SttEngine<br/>WhisperModel]
|
||||||
|
end
|
||||||
|
|
||||||
|
HA -- TCP 10300 --> WY
|
||||||
|
ESP --> HTTP
|
||||||
|
OAI --> HTTP
|
||||||
|
CLI --> HTTP
|
||||||
|
CLI --> WS
|
||||||
|
ESP --> WS
|
||||||
|
WS --> SEG
|
||||||
|
WY --> W
|
||||||
|
HTTP --> W
|
||||||
|
SEG -- Äußerung --> W
|
||||||
|
W -- Thread --> E
|
||||||
|
E -- CTranslate2 --> GPU[(GPU / CPU)]
|
||||||
|
```
|
||||||
|
|
||||||
|
Alle Schnittstellen enden im **selben `TranscriptionWorker`** und damit in **einem** geladenen Modell.
|
||||||
|
Der Worker serialisiert die Inferenz (eine GPU-Aufgabe zur Zeit) und begrenzt die Anzahl wartender
|
||||||
|
Aufträge.
|
||||||
|
|
||||||
|
### Ablauf einer HTTP-Anfrage
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant L as Event-Loop (FastAPI)
|
||||||
|
participant D as to_thread (Dekodieren)
|
||||||
|
participant W as Worker-Thread
|
||||||
|
participant E as SttEngine
|
||||||
|
C->>L: POST /transcribe (Body)
|
||||||
|
L->>L: Größe prüfen (413)
|
||||||
|
L->>D: decode_audio_bytes (PyAV)
|
||||||
|
D-->>L: float32 16 kHz
|
||||||
|
L->>L: pending >= max_queue? → 503
|
||||||
|
L->>W: run_in_executor(engine.transcribe)
|
||||||
|
Note over L: Event-Loop frei für /health, WS, Wyoming
|
||||||
|
W->>E: transcribe (mit _run_lock)
|
||||||
|
E-->>W: Transcript
|
||||||
|
W-->>L: Transcript
|
||||||
|
L-->>C: JSON
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Modul `stt_core.py`
|
||||||
|
|
||||||
|
Reine Logik: **kein `print`, kein `sys.exit`, keine Netzwerkzugriffe**. Fehler als Exceptions,
|
||||||
|
Ergebnisse als Dataclasses. Schwere Importe (`faster_whisper`, `scipy`, `soxr`) erfolgen erst in den
|
||||||
|
Funktionen, die sie brauchen — `import stt_core` ist dadurch schnell.
|
||||||
|
|
||||||
|
### 4.1 Audio-Hilfen
|
||||||
|
|
||||||
|
| Funktion/Klasse | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `pcm16_to_float(data, channels, channel)` | s16le → float32 mono; Kanal auswählen oder mitteln; unvollständige Frames verwerfen |
|
||||||
|
| `float_to_pcm16(audio)` | Gegenrichtung (Tests, Clients) |
|
||||||
|
| `resample(audio, src, dst)` | ganze Signale (`scipy.signal.resample_poly`) |
|
||||||
|
| `StreamResampler(src, dst)` | fortlaufende Stücke ohne Randartefakte (`soxr.ResampleStream`); `process(chunk, last=True)` leert den Filter |
|
||||||
|
| `decode_audio_bytes(data)` | beliebige Container über `faster_whisper.decode_audio` (PyAV) → 16 kHz mono; wirft `AudioDecodeError` |
|
||||||
|
|
||||||
|
Konstante `SAMPLE_RATE = 16000` ist das interne Format überall.
|
||||||
|
|
||||||
|
### 4.2 Ergebnisobjekte
|
||||||
|
|
||||||
|
`Segment(start, end, text, avg_logprob, no_speech_prob)` und
|
||||||
|
`Transcript(text, language, language_probability, duration, processing_s, segments, dropped_segments)`
|
||||||
|
mit `to_dict(with_segments=True)`. Die JSON-Antworten der Schnittstellen sind direkt daraus abgeleitet —
|
||||||
|
neue Felder hier erscheinen automatisch in `/transcribe` und im WebSocket.
|
||||||
|
|
||||||
|
### 4.3 Halluzinationsfilter
|
||||||
|
|
||||||
|
Whisper erzeugt bei Stille oder Rauschen typische Floskeln aus seinen Trainingsdaten (Untertitel).
|
||||||
|
`is_hallucination(seg)` verwirft ein Segment, wenn
|
||||||
|
|
||||||
|
1. der normalisierte Text (klein, Satzzeichen weg) **vollständig** einem Muster aus
|
||||||
|
`_HALLUCINATION_PATTERNS` entspricht (z. B. „untertitel im auftrag des zdf 2021“), oder leer ist, oder
|
||||||
|
2. Whisper selbst unsicher ist **und** Stille vermutet: `no_speech_prob > 0.6 and avg_logprob < -0.8`.
|
||||||
|
|
||||||
|
„Vollständig“ ist Absicht: „Die Untertitel im Fernsehen sind zu klein.“ bleibt erhalten.
|
||||||
|
Verworfene Segmente zählt `Transcript.dropped_segments`; mit `STT_LOG_LEVEL=DEBUG` werden sie geloggt.
|
||||||
|
|
||||||
|
### 4.4 `EngineConfig` und `SttEngine`
|
||||||
|
|
||||||
|
`EngineConfig` bündelt alle Modellparameter; `EngineConfig.from_env()` liest `STT_*`.
|
||||||
|
|
||||||
|
**Laden** (`SttEngine.load`, idempotent, durch `_load_lock` geschützt):
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[load] --> B{Modell schon geladen?}
|
||||||
|
B -- ja --> Z[fertig]
|
||||||
|
B -- nein --> C[_load_attempts]
|
||||||
|
C --> D{device auto/cuda<br/>und CUDA-GPU vorhanden?}
|
||||||
|
D -- ja --> E[Versuch: cuda, model, float16]
|
||||||
|
D -- nein --> F
|
||||||
|
E -- Fehler --> F[Versuch: cpu, cpu_model*, int8]
|
||||||
|
E -- ok --> R[status = ready]
|
||||||
|
F -- ok --> R
|
||||||
|
F -- Fehler --> X[status = error, RuntimeError]
|
||||||
|
```
|
||||||
|
|
||||||
|
\* bei `device=cpu` wird `model` statt `cpu_model` verwendet — wer CPU explizit wählt, bekommt sein Modell.
|
||||||
|
|
||||||
|
Status-Felder für `/health`: `status` (`idle|loading|ready|error`), `error`, `active_model`,
|
||||||
|
`active_device`, `active_compute_type`, `load_seconds`.
|
||||||
|
|
||||||
|
**Transkribieren** (`SttEngine.transcribe`, blockierend):
|
||||||
|
|
||||||
|
- `< 100 ms` Audio → sofort leeres `Transcript`, ohne das Modell anzufassen.
|
||||||
|
- Sprache über `resolve_language`: `None` → Config, `"auto"`/`""` → Erkennung, `"de-DE"` → `"de"`.
|
||||||
|
- Aufruf von `WhisperModel.transcribe` unter `_run_lock` mit:
|
||||||
|
|
||||||
|
| Parameter | Wert | Grund |
|
||||||
|
|---|---|---|
|
||||||
|
| `vad_filter` | `True` (Standard) | Stille innerhalb des Audios überspringen → schneller, weniger Halluzinationen |
|
||||||
|
| `vad_parameters` | `min_silence_duration_ms=500, speech_pad_ms=200` | Wörter an Pausen nicht abschneiden |
|
||||||
|
| `condition_on_previous_text` | `False` | verhindert Wiederholungsschleifen über 30-s-Fenster hinweg |
|
||||||
|
| `beam_size` | Config (5) | Genauigkeit; `warmup` nutzt 1 |
|
||||||
|
| `initial_prompt` | Anfrage oder Config | Eigennamen |
|
||||||
|
| `hotwords` | nur Wyoming | Namen/Begriffe aus Home Assistant |
|
||||||
|
|
||||||
|
- **Wichtig:** `segments` ist ein Generator — die eigentliche Rechenarbeit passiert erst bei `list(...)`,
|
||||||
|
deshalb liegt das innerhalb des Locks.
|
||||||
|
|
||||||
|
`warmup()` lädt das Modell und transkribiert 1 s leises Rauschen (ohne VAD), damit CUDA-Kernel und
|
||||||
|
Decoder initialisiert sind und die erste echte Anfrage schnell ist.
|
||||||
|
|
||||||
|
### 4.5 Streaming-Segmentierung
|
||||||
|
|
||||||
|
**`SileroProb`** liefert die Sprachwahrscheinlichkeit je 512-Sample-Fenster (32 ms) mit dem in
|
||||||
|
faster-whisper mitgelieferten Silero-VAD (ONNX, CPU, kein Download). Besonderheit: Das Modell leitet
|
||||||
|
den 64-Sample-Kontext eines Fensters aus dem **vorherigen Fenster im selben Aufruf** ab. Damit das über
|
||||||
|
Aufrufgrenzen stimmt, wird das letzte Fenster des vorigen Aufrufs vorangestellt und dessen Ergebnis
|
||||||
|
verworfen.
|
||||||
|
|
||||||
|
**`UtteranceSegmenter`** ist eine Zustandsmaschine über diesen Fenstern:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Warten
|
||||||
|
Warten --> Warten: Fenster in Pre-Roll-Puffer, p < threshold → voiced = 0
|
||||||
|
Warten --> Sprache: voiced ≥ min_speech_ms, → Ereignis speech_start, Pre-Roll wird Äußerungsanfang
|
||||||
|
Sprache --> Sprache: Fenster anhängen, p < threshold − 0,15 → silent += 1, sonst silent = 0
|
||||||
|
Sprache --> Warten: silent ≥ silence_ms, → utterance (reason silence)
|
||||||
|
Sprache --> Warten: Länge ≥ max_utterance_s, → utterance (reason max_length)
|
||||||
|
Sprache --> Warten: flush(), → utterance (reason flush)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Hysterese:** Start bei `p ≥ threshold`, Stille erst bei `p < threshold − 0,15` → kein Flattern.
|
||||||
|
- **Pre-Roll:** Ringpuffer der letzten `pre_roll_ms + min_speech_ms` → Anlaute bleiben erhalten.
|
||||||
|
- **Stream-Zeit** (`time_s`) zählt verarbeitete Samples, nicht die Uhr → Dateien dürfen schneller als
|
||||||
|
Echtzeit gestreamt werden.
|
||||||
|
- `prob_fn` ist injizierbar (Tests nutzen `EnergyProb`), `feed()` gibt eine Liste von `VadEvent` zurück.
|
||||||
|
- Unvollständige Fenster bleiben in `_pending` bis zum nächsten `feed()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Modul `stt_service.py`
|
||||||
|
|
||||||
|
### 5.1 App-Fabrik
|
||||||
|
|
||||||
|
```python
|
||||||
|
app = create_app(engine=None, settings=None, vad_config=None, prob_fn_factory=None)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ohne Argumente wird alles aus der Umgebung gebaut (`EngineConfig.from_env()`, `ServiceSettings.from_env()`,
|
||||||
|
`VadConfig.from_env()`, `SileroProb`). Tests übergeben `FakeEngine` und `EnergyProb`. Auf Modulebene
|
||||||
|
steht `app = create_app()` für `uvicorn stt_service:app` — das ist billig, weil erst die **Lifespan**
|
||||||
|
das Modell lädt.
|
||||||
|
|
||||||
|
**Lifespan:**
|
||||||
|
1. `STT_PRELOAD`: `engine.warmup` als **erster Job im Worker** starten, **ohne** zu warten. Der Dienst
|
||||||
|
antwortet sofort (`/health` → `loading`); frühe Anfragen reihen sich hinter dem Laden ein.
|
||||||
|
2. Wyoming-Server starten, falls Port gesetzt (Fehler → nur Log, der HTTP-Dienst läuft trotzdem).
|
||||||
|
3. Beim Beenden: Wyoming stoppen, Preload abbrechen, Executor herunterfahren.
|
||||||
|
|
||||||
|
### 5.2 `TranscriptionWorker`
|
||||||
|
|
||||||
|
- `ThreadPoolExecutor(max_workers=1)` → genau eine Inferenz gleichzeitig (GPU-schonend,
|
||||||
|
vorhersagbarer Speicher).
|
||||||
|
- `pending` zählt laufende + wartende Aufträge. Er wird **nur im Event-Loop** verändert, daher ohne Lock.
|
||||||
|
- `transcribe()` wirft `Overloaded`, wenn `pending ≥ max_queue` → HTTP 503 bzw. WS-`error`.
|
||||||
|
- `run(fn, …)` für beliebige blockierende Aufgaben im selben Thread (z. B. `warmup`).
|
||||||
|
|
||||||
|
### 5.3 HTTP-Endpunkte
|
||||||
|
|
||||||
|
`_run_transcription` übersetzt Ausnahmen in HTTP-Status (`Overloaded` → 503, `RuntimeError` → 500).
|
||||||
|
`_decode` dekodiert in `asyncio.to_thread` (PyAV kostet bei langen Dateien CPU) und macht aus
|
||||||
|
`AudioDecodeError` einen 400. `_check_size` erzwingt `max_upload_mb`.
|
||||||
|
|
||||||
|
### 5.4 WebSocket: `_StreamSession`
|
||||||
|
|
||||||
|
Eine Instanz pro Verbindung. Zwei nebenläufige Tasks:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
RX["run() — Empfangsschleife"] -- Binär --> A[_on_audio:<br/>pcm16_to_float → StreamResampler<br/>→ UtteranceSegmenter.feed]
|
||||||
|
RX -- Text --> B[_on_control:<br/>start / end / close]
|
||||||
|
A -- speech_start --> SEND
|
||||||
|
A -- utterance --> Q[(jobs: asyncio.Queue)]
|
||||||
|
B -- flush / done-Marker --> Q
|
||||||
|
Q --> C["_consume() — Task"]
|
||||||
|
C -- worker.transcribe --> W[TranscriptionWorker]
|
||||||
|
C -- transcript / error / done --> SEND[send mit send_lock]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Die Empfangsschleife **wartet nie auf Whisper** → Audio wird auch während der Transkription
|
||||||
|
angenommen; Äußerungen werden in Reihenfolge abgearbeitet.
|
||||||
|
- `send_lock`, weil beide Tasks senden.
|
||||||
|
- Queue-Einträge: `(audio, reason, t)` · `"done"` (Marker nach `end`) · `None` (Ende).
|
||||||
|
- Ohne VAD sammelt `buffer` bis `end` bzw. bis `max_utterance_s` erreicht ist.
|
||||||
|
- `start` baut Resampler und Segmenter neu (setzt die Sitzung zurück).
|
||||||
|
- Beim Verbindungsende: `None` einreihen, `_consume` bis 120 s ausstehende Transkripte abarbeiten lassen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Modul `stt_wyoming.py`
|
||||||
|
|
||||||
|
`SttEventHandler(AsyncEventHandler)` — eine Instanz pro TCP-Verbindung.
|
||||||
|
|
||||||
|
| Ereignis | Aktion |
|
||||||
|
|---|---|
|
||||||
|
| `describe` | `build_info(engine)` → `Info` mit `AsrProgram("speech-to-text")`, Modellname, Whisper-Sprachen |
|
||||||
|
| `transcribe` | `language` merken; `transcript_names + transcript_terms` → `hotwords` |
|
||||||
|
| `audio-start` | Puffer und `AudioChunkConverter(16000 Hz, 16 Bit, mono)` neu |
|
||||||
|
| `audio-chunk` | konvertieren und anhängen |
|
||||||
|
| `audio-stop` | `worker.transcribe` → `Transcript`-Ereignis → `return False` (Verbindung schließen, HA-Konvention) |
|
||||||
|
|
||||||
|
Fehler bei der Transkription führen zu einem **leeren** Transkript statt zu einem Abbruch — Home Assistant
|
||||||
|
würde sonst auf eine Antwort warten. `start_server(worker, host, port)` gibt den `AsyncTcpServer` zurück;
|
||||||
|
Port `0` ist in Tests erlaubt (freien Port über `server._server.sockets[0]` ermitteln).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Modul `stt_cli.py`
|
||||||
|
|
||||||
|
| Funktion | Aufgabe |
|
||||||
|
|---|---|
|
||||||
|
| `main` | Argumente, Modus-Auswahl, Fehler → Exit-Code, Hinweis bei nicht erreichbarem Dienst |
|
||||||
|
| `service_file` | `POST /transcribe` mit Datei als Body (Streaming-Upload über `requests`) |
|
||||||
|
| `service_mic` | Mikrofon → `/ws/stream` (synchroner `websockets`-Client, Empfänger-Thread) |
|
||||||
|
| `local_files` / `local_mic` | wie oben, aber `SttEngine` + `UtteranceSegmenter` im Prozess; Inferenz im `ThreadPoolExecutor(1)` |
|
||||||
|
| `Microphone` | `sounddevice.InputStream` mit Callback → `queue.Queue` von s16le-Mono-Blöcken (100 ms) |
|
||||||
|
| `resolve_input` | Gerätewahl: kein `-d` → ALSA-Gerät `pulse` (folgt PipeWire-Default), Zahl → Index, Text → Namenssuche |
|
||||||
|
|
||||||
|
**Threads im Mikrofon-Modus mit Dienst:** Audio-Callback (PortAudio) → Queue → Hauptthread sendet;
|
||||||
|
ein Empfänger-Thread liest Ereignisse und schreibt Transkripte auf stdout. Keine Stelle blockiert die
|
||||||
|
Aufnahme; läuft die Queue (60 s) voll, werden Blöcke gezählt verworfen.
|
||||||
|
|
||||||
|
**Warum `pulse` statt PortAudio-Default?** PortAudio wählt auf PipeWire-Systemen die erste ALSA-Karte
|
||||||
|
(hier: Onboard-ALC1220), nicht das eingestellte Mikrofon. Über `pulse` wird mit 16 kHz aufgenommen
|
||||||
|
(PipeWire resampelt); der Name der Standardquelle kommt aus `pactl get-default-source` und dient der
|
||||||
|
ReSpeaker-Erkennung (→ Kanal 1).
|
||||||
|
|
||||||
|
**Ausgabevertrag:** stdout nur Ergebnisse (Text oder JSON-Zeile), stderr alles andere.
|
||||||
|
Exit-Codes `0/1/2/130` (siehe Bedienungsanleitung).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Nebenläufigkeit — die wichtigsten Regeln
|
||||||
|
|
||||||
|
1. **Nie** `engine.transcribe` (oder anderes Blockierendes) direkt in einer `async`-Funktion aufrufen —
|
||||||
|
immer über `worker.transcribe` / `worker.run` bzw. `asyncio.to_thread`.
|
||||||
|
2. `worker.pending` nur im Event-Loop verändern.
|
||||||
|
3. In einer WebSocket-Sitzung nur über `_StreamSession.send` senden (Lock).
|
||||||
|
4. `SttEngine` ist thread-sicher (`_load_lock`, `_run_lock`), aber der Dienst verlässt sich zusätzlich
|
||||||
|
auf den Ein-Thread-Worker für die Warteschlangenlogik.
|
||||||
|
5. `UtteranceSegmenter` und `StreamResampler` sind **nicht** thread-sicher — eine Instanz pro Stream.
|
||||||
|
6. Silero-VAD läuft im Event-Loop (gemessen ≈ 0,4 ms Rechenzeit pro 100 ms Audio). Bei sehr vielen gleichzeitigen
|
||||||
|
Streams wäre das der erste Kandidat für einen eigenen Thread.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Konfiguration
|
||||||
|
|
||||||
|
Alle Einstellungen sind Umgebungsvariablen (vollständige Tabelle:
|
||||||
|
[Bedienungsanleitung §9](../BEDIENUNGSANLEITUNG.md#9-einstellungen)). Gelesen werden sie an genau drei
|
||||||
|
Stellen:
|
||||||
|
|
||||||
|
| Klasse | Variablen |
|
||||||
|
|---|---|
|
||||||
|
| `stt_core.EngineConfig.from_env` | `STT_MODEL`, `STT_DEVICE`, `STT_COMPUTE_TYPE`, `STT_DEVICE_INDEX`, `STT_CPU_MODEL`, `STT_CPU_THREADS`, `STT_LANGUAGE`, `STT_BEAM_SIZE`, `STT_INITIAL_PROMPT`, `STT_DOWNLOAD_ROOT` |
|
||||||
|
| `stt_core.VadConfig.from_env` | `STT_VAD_THRESHOLD`, `STT_VAD_MIN_SPEECH_MS`, `STT_VAD_SILENCE_MS`, `STT_VAD_PRE_ROLL_MS`, `STT_VAD_MAX_UTTERANCE_S` |
|
||||||
|
| `stt_service.ServiceSettings.from_env` | `STT_MAX_QUEUE`, `STT_MAX_UPLOAD_MB`, `STT_PRELOAD`, `STT_WYOMING_HOST`, `STT_WYOMING_PORT` |
|
||||||
|
|
||||||
|
Außerdem direkt: `STT_LOG_LEVEL`, `STT_HOST`, `STT_PORT` (`stt_service.py`), `STT_URL` (`stt_cli.py`).
|
||||||
|
Hilfsfunktion `stt_core._env(name, default, cast)`: leere Werte gelten als „nicht gesetzt“,
|
||||||
|
`bool` akzeptiert `1/true/yes/on`.
|
||||||
|
|
||||||
|
Neue Einstellung = Feld in der passenden Dataclass + Zeile in `from_env` + Tabelle in der
|
||||||
|
Bedienungsanleitung + Test (`test_engine_config_from_env` als Muster).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Entwicklungsumgebung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/Python_Programs/SpeechToText
|
||||||
|
python3 -m venv --system-site-packages .venv
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Die venv erbt auf dem Entwicklungsrechner faster-whisper, FastAPI, onnxruntime usw. aus
|
||||||
|
`~/miniforge3`; `wyoming` ist nur in der venv installiert. **Immer `.venv/bin/python` verwenden**, sonst
|
||||||
|
fehlt `wyoming` (Wyoming-Tests werden dann übersprungen, der Dienst startet ohne Wyoming).
|
||||||
|
|
||||||
|
**Dienst im Entwicklungsbetrieb:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CUDA_VISIBLE_DEVICES=GPU-83ba6d1f-ebbe-f8c8-1433-153e0ebc3866 STT_LOG_LEVEL=DEBUG .venv/bin/python stt_service.py
|
||||||
|
# schneller Start ohne GPU/Download:
|
||||||
|
STT_MODEL=tiny STT_DEVICE=cpu STT_WYOMING_PORT= .venv/bin/uvicorn stt_service:app --port 9998 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
**GPU-Umgebung des Entwicklungsrechners:** T600 + 2× RTX 3090. `nvidia-smi` zählt in PCI-Reihenfolge,
|
||||||
|
CUDA „fastest first“ — Indizes stimmen nicht überein, daher GPU per UUID pinnen. GPU 1 ist durch
|
||||||
|
llama.cpp/Ollama nahezu voll; GPU 2 teilen sich chatterbox-tts, Open Notebook (STT/TTS) und dieser Dienst
|
||||||
|
(`large-v3-turbo` ≈ 2,4 GB). Hugging-Face-Modelle liegen unter `$HF_HOME/hub`.
|
||||||
|
|
||||||
|
**Schnelle Handtests:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
espeak-ng -v de -w /tmp/t.wav "Schalte das Licht im Wohnzimmer ein."
|
||||||
|
curl --data-binary @/tmp/t.wav localhost:9998/transcribe | python3 -m json.tool
|
||||||
|
.venv/bin/python stt_cli.py /tmp/t.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
.venv/bin/python -m pytest # 36 Unit-Tests, ~9 s, ohne Modell
|
||||||
|
.venv/bin/python -m pytest -m integration # 4 Integrationstests, ~20 s, Whisper small auf CPU
|
||||||
|
.venv/bin/python -m pytest tests/test_service.py -k ws -v
|
||||||
|
STT_TEST_MODEL=large-v3-turbo STT_TEST_DEVICE=cuda .venv/bin/python -m pytest -m integration
|
||||||
|
```
|
||||||
|
|
||||||
|
`pytest.ini` schließt `integration` standardmäßig aus.
|
||||||
|
|
||||||
|
### Helfer in `tests/conftest.py`
|
||||||
|
|
||||||
|
| Helfer | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `FakeEngine(delay=0)` | Engine ohne Modell; Text = `"audio <Dauer>s"`; zeichnet Aufrufe in `calls` auf; `delay` simuliert Rechenzeit |
|
||||||
|
| `EnergyProb` | deterministische Ersatz-VAD: Fenster-RMS > 0,05 = Sprache |
|
||||||
|
| `make_wav(audio, rate, channels, width)` | float32 → WAV-Bytes (16/32 Bit) |
|
||||||
|
| `tone(sec, freq, amp, rate)` / `silence(sec, rate)` | synthetische Signale |
|
||||||
|
| `espeak_wav(text, voice)` | echte (synthetische) Sprache für Integrationstests; `None` ohne espeak-ng |
|
||||||
|
|
||||||
|
`conftest.py` setzt `STT_WYOMING_PORT=""`, damit das beim Import erzeugte Modul-`app` keinen TCP-Port
|
||||||
|
belegt.
|
||||||
|
|
||||||
|
### Was wie getestet wird
|
||||||
|
|
||||||
|
| Bereich | Muster |
|
||||||
|
|---|---|
|
||||||
|
| Kernlogik | direkte Funktionsaufrufe (`test_core.py`) |
|
||||||
|
| HTTP | `TestClient(create_app(FakeEngine(), ServiceSettings(wyoming_port=None), …))` |
|
||||||
|
| Nebenläufigkeit (503, nicht blockieren) | `httpx.ASGITransport` + `asyncio.gather`, `FakeEngine(delay=…)`; auf `worker.pending` warten statt fester `sleep`s |
|
||||||
|
| WebSocket | `client.websocket_connect("/ws/stream")`, Hilfsfunktion `_receive_until(ws, "done")` |
|
||||||
|
| Wyoming | echter `start_server(…, port=0)` + `wyoming.client.AsyncTcpClient` |
|
||||||
|
| Ende-zu-Ende | `test_integration.py`: espeak-Sprache → Silero-Segmentierung → Whisper |
|
||||||
|
|
||||||
|
Neue Funktionen bitte mit Unit-Test ohne Modell absichern; nur was wirklich Whisper braucht, als
|
||||||
|
`@pytest.mark.integration`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Erweitern: typische Aufgaben
|
||||||
|
|
||||||
|
**Neuer HTTP-Endpunkt** — in `create_app` definieren, Audio über `_decode` bzw. `core.pcm16_to_float`
|
||||||
|
beschaffen, Transkription über `_run_transcription`. Test in `test_service.py` mit `_client()`. In
|
||||||
|
`docs/API.md` dokumentieren.
|
||||||
|
|
||||||
|
**Neuer Transkriptionsparameter** (z. B. Wort-Zeitstempel) — Parameter in `SttEngine.transcribe`
|
||||||
|
ergänzen und an `model.transcribe` durchreichen; bei Bedarf Felder in `Segment`/`Transcript`; in den
|
||||||
|
Endpunkten als Query/Form/`start`-Feld annehmen. `FakeEngine.transcribe` akzeptiert `**kwargs`, bestehende
|
||||||
|
Tests laufen weiter.
|
||||||
|
|
||||||
|
**Halluzinationsmuster ergänzen** — Regex (klein, ohne Satzzeichen am Ende) in `_HALLUCINATION_PATTERNS`,
|
||||||
|
Parametrisierung in `test_hallucinations_are_detected` **und** ein Gegenbeispiel echter Sprache in
|
||||||
|
`test_real_speech_is_kept`.
|
||||||
|
|
||||||
|
**Andere VAD** — Klasse mit `__call__(windows: np.ndarray) -> np.ndarray` (eine Wahrscheinlichkeit je
|
||||||
|
512er-Fenster) und optional `reset()`; im Dienst über `create_app(prob_fn_factory=…)` einsetzen.
|
||||||
|
|
||||||
|
**Andere Engine** (z. B. anderes STT-Backend) — Unterklasse von `SttEngine`, die `load` und `transcribe`
|
||||||
|
überschreibt und ein `Transcript` liefert (`FakeEngine` zeigt das minimale Muster).
|
||||||
|
|
||||||
|
**Neuer Client** — gegen [docs/API.md](API.md) programmieren; `stt_cli.py` ist die Referenzimplementierung
|
||||||
|
für Datei-Upload und WebSocket-Streaming.
|
||||||
|
|
||||||
|
**Version erhöhen** — `stt_core.__version__` (wird von `/health`, FastAPI und Wyoming übernommen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Designentscheidungen
|
||||||
|
|
||||||
|
| Entscheidung | Begründung | Verworfene Alternative |
|
||||||
|
|---|---|---|
|
||||||
|
| Äußerungsende per Silero-VAD | natürliche Bedienung, geringe Latenz, für Smarthome üblich | Stoppwort (legacy): unzuverlässig an Blockgrenzen, doppelte Transkription |
|
||||||
|
| Ein Worker-Thread | GPU-Speicher vorhersagbar, keine Konkurrenz um den Kontext; ~0,2 s pro Befehl reicht für einen Haushalt | mehrere Worker (`num_workers`): mehr VRAM, kaum Gewinn auf einer geteilten GPU |
|
||||||
|
| Sofortiges 503 bei voller Queue | „nicht blockieren“: Clients bekommen schnell eine Antwort und können neu versuchen | unbegrenzte Queue: Latenz wächst unbemerkt |
|
||||||
|
| `large-v3-turbo` als Standard | nahezu `large-v3`-Qualität, deutlich schneller, ~2,4 GB statt ~4 GB | `large-v3`: langsamer; `medium`: schlechter bei Deutsch |
|
||||||
|
| CPU-Fallback mit `small` | Dienst bleibt nutzbar, wenn die GPU belegt ist; `small` ist auf CPU noch schnell | Abbruch: Smarthome ohne Spracherkennung |
|
||||||
|
| `condition_on_previous_text=False` | verhindert Wiederholungsschleifen | Standard `True` |
|
||||||
|
| Preload ohne Warten | Dienst sofort erreichbar, `/health` zeigt Fortschritt | blockierender Start: systemd-Timeouts, keine Diagnose |
|
||||||
|
| Wyoming im selben Prozess | ein Modell im Speicher, eine Konfiguration | separater Wyoming-Dienst (`wyoming-faster-whisper`): zweites Modell |
|
||||||
|
| OpenAI-kompatibler Endpunkt | vorhandene OpenAI-Clients (SDK, Voice-Assistant v3 …) nutzbar, nur Basis-URL `…/v1` setzen | nur eigene API |
|
||||||
|
| Roh-Endpunkt `/transcribe` | Mikrocontroller können kein bequemes multipart | nur multipart |
|
||||||
|
| CLI über `pulse`-Gerät | folgt der Systemeinstellung; PortAudio-Default ist falsch | PortAudio-Default (legacy-Fehler) |
|
||||||
|
| Keine Authentifizierung (vorerst) | Heimnetz-Betrieb, einfache Clients (ESP32) | Token: siehe nächste Schritte |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Bekannte Grenzen und mögliche nächste Schritte
|
||||||
|
|
||||||
|
**Grenzen**
|
||||||
|
|
||||||
|
- Wörtlich wiederholte Sätze innerhalb eines 30-s-Fensters fasst Whisper teilweise zusammen.
|
||||||
|
- Keine Zwischenergebnisse während des Sprechens; Transkripte kommen pro Äußerung.
|
||||||
|
- Keine Authentifizierung, kein TLS.
|
||||||
|
- Ein Modell pro Dienstinstanz; Modellwechsel = Neustart.
|
||||||
|
- Wyoming protokolliert Transkripttexte auf `INFO`.
|
||||||
|
- Die Mikrofonpfade der CLI sind mit synthetischer Sprache und Pegelmessung getestet, der WebSocket-Pfad
|
||||||
|
mit echter Sprache in Echtzeit-Simulation — nicht automatisiert mit echtem Mikrofon.
|
||||||
|
|
||||||
|
**Mögliche nächste Schritte**
|
||||||
|
|
||||||
|
- Optionales Bearer-Token (`STT_API_TOKEN`) für HTTP/WebSocket.
|
||||||
|
- Metriken (`/metrics` im Prometheus-Format: Latenzen, Queue, Fehler).
|
||||||
|
- Zwischenergebnisse (partielle Transkripte) während langer Äußerungen.
|
||||||
|
- MCP-Adapter analog zu chatterbox-tts-cli (`mcp_adapter.py`).
|
||||||
|
- Remote-Repository auf kitux.de (Forgejo).
|
||||||
|
- Anbindung als STT-Provider im Voice-Assistant v3 (OpenAI-kompatibel bereits möglich).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Konventionen
|
||||||
|
|
||||||
|
- **Sprache:** Kommentare, Docstrings, Doku, Log-/Fehlermeldungen und Commit-Nachrichten auf Deutsch;
|
||||||
|
Bezeichner englisch.
|
||||||
|
- **Kern ohne I/O:** `stt_core` gibt nichts aus und beendet nichts — Rückgabewerte und Exceptions.
|
||||||
|
- **Logging** über `logging` (`stt`, `stt.service`, `stt.wyoming`), nicht `print` (Ausnahme: CLI-Ausgabe).
|
||||||
|
- **Typen:** Type Hints und `from __future__ import annotations`; Datenobjekte als `@dataclass`.
|
||||||
|
- **Konfiguration** nur über Umgebungsvariablen in den `from_env`-Methoden.
|
||||||
|
- **Tests** zu jeder Änderung; Unit-Tests ohne Modell, Integration mit Marker.
|
||||||
|
- **Commits:** kleine, thematische Commits; Autor `dschlueter <dschlueter@kitux.de>`.
|
||||||
|
- **Leere Datei `3`:** entsteht durch einen System-Bug, gehört nie in einen Commit und darf gelöscht werden.
|
||||||
Loading…
Reference in a new issue