CLI: --until-enter und optionale LLM-Korrektur (--llm-correct)
- --until-enter: Aufnahme mit beliebig langen Pausen, Enter beendet, Ausgabe als ein Text - stt_llm.py: Nachkorrektur per lokalem LLM (Standard gemma4:12b über Ollama nativ mit kleinem Kontext; OpenAI-kompatible Server möglich), strenger Prompt mit Beispielen, Ähnlichkeitsprüfung und Fehler-Fallback auf den Rohtext - Output-Klasse in der CLI: Korrektur in eigenem Thread, Reihenfolge bleibt erhalten - Tests für die LLM-Korrektur (13 Unit, 1 Integration) - Doku: Bedienungsanleitung, README, Entwicklerdoku (inkl. Modellvergleich), CLAUDE.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
340100210d
commit
6ebea4b745
1 changed files with 672 additions and 24 deletions
|
|
@ -50,11 +50,13 @@ SpeechToText/
|
|||
├── 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
|
||||
├── stt_llm.py optionale LLM-Nachkorrektur (nur CLI --llm-correct)
|
||||
├── 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_llm.py LLM-Korrektur mit geskripteten Antworten (+ 1 Integrationstest mit Ollama)
|
||||
│ └── test_integration.py echtes Whisper + Silero (Marker "integration")
|
||||
├── deploy/stt-service.service systemd-User-Unit
|
||||
├── docs/API.md Schnittstellen-Referenz
|
||||
|
|
@ -76,9 +78,12 @@ flowchart LR
|
|||
service -. Import zur Laufzeit .-> wyoming[stt_wyoming.py]
|
||||
wyoming --> core
|
||||
cli -- HTTP/WS --> service
|
||||
cli -. "nur mit Option llm-correct" .-> llm[stt_llm.py]
|
||||
llm -- HTTP --> ollama[(Ollama / OpenAI-kompatibel)]
|
||||
```
|
||||
|
||||
`stt_core` importiert nichts aus dem Projekt. `stt_wyoming` wird erst beim Start importiert, damit der
|
||||
`stt_core` importiert nichts aus dem Projekt; `stt_llm` nutzt aus dem Kern nur `_env`. Dienst und Wyoming
|
||||
wissen nichts von der LLM-Korrektur. `stt_wyoming` wird erst beim Start importiert, damit der
|
||||
Dienst ohne das Paket `wyoming` lauffähig bleibt.
|
||||
|
||||
---
|
||||
|
|
@ -343,6 +348,7 @@ Port `0` ist in Tests erlaubt (freien Port über `server._server.sockets[0]` erm
|
|||
| `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)` |
|
||||
| `start_enter_watcher` / `emit_joined` | `--until-enter`: Thread wartet auf Enter; Abschnitte sammeln und am Ende als ein Ergebnis ausgeben (Dienst: `end` senden, auf `done` warten; lokal: `segmenter.flush()`, Futures in Reihenfolge) |
|
||||
| `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 |
|
||||
|
||||
|
|
@ -360,6 +366,54 @@ Exit-Codes `0/1/2/130` (siehe Bedienungsanleitung).
|
|||
|
||||
---
|
||||
|
||||
### LLM-Nachkorrektur (`stt_llm.py`)
|
||||
|
||||
Optionaler Zusatz, bewusst **außerhalb** von Kern und Dienst: Smarthome-, ESP32- und Wyoming-Pfade bleiben
|
||||
schnell und deterministisch. Eingebunden nur über `Output` in `stt_cli.py` (`--llm-correct`).
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
T[Transkript] --> O[Output.put]
|
||||
O -- ohne LLM --> E[emit stdout]
|
||||
O -- mit LLM --> X[ThreadPoolExecutor 1<br/>Reihenfolge bleibt]
|
||||
X --> C[LlmCorrector.correct]
|
||||
C --> S[split_chunks<br/>≤ 1200 Zeichen]
|
||||
S --> CH[_chat<br/>Ollama /api/chat oder /v1/chat/completions]
|
||||
CH --> CL[clean_output]
|
||||
CL --> G{_accept<br/>Ähnlichkeit ≥ 0,75<br/>Länge ±30 %}
|
||||
G -- ja --> K[korrigierter Text]
|
||||
G -- nein --> R[Rohtext]
|
||||
CH -- Exception --> R
|
||||
K --> E
|
||||
R --> E
|
||||
```
|
||||
|
||||
| Baustein | Aufgabe |
|
||||
|---|---|
|
||||
| `LlmConfig` / `from_env` | URL, Modell, `api` (`auto`/`ollama`/`openai`), `keep_alive`, `num_ctx`, Timeout, Schwellen (`STT_LLM_*`) |
|
||||
| `SYSTEM_PROMPT` + `FEW_SHOT` | nur Erkennungsfehler korrigieren; Diktat in `<transkript>`-Tags; Beispiel mit „Anweisung“ im Diktat |
|
||||
| `LlmCorrector._detect_api` | `…/v1` → OpenAI-kompatibel; sonst `GET /api/version` → Ollama |
|
||||
| `LlmCorrector._chat` | Ollama nativ mit `think: false`, `keep_alive`, `num_ctx`, `temperature 0`; sonst Chat Completions |
|
||||
| `clean_output` | entfernt Tags, umschließende Anführungszeichen, „Korrigierter Text:“ |
|
||||
| `_accept` | Ähnlichkeit (`difflib`) und Längenänderung → sonst Rohtext |
|
||||
| `correct` | wirft nie; `Correction(text, raw_text, status, detail, llm_text, similarity, seconds, model)` |
|
||||
|
||||
**Warum Ollama nativ statt `/v1`?** Auf dem Entwicklungsrechner ist Ollama global mit
|
||||
`OLLAMA_CONTEXT_LENGTH=131072` und `OLLAMA_NUM_PARALLEL=4` konfiguriert. Über `/v1` lässt sich der Kontext
|
||||
nicht pro Anfrage setzen — die Modelle liefen dann zu 75–90 % auf der CPU (5–8 s pro Satz, `mistral-nemo`
|
||||
95 GB). Mit `num_ctx=4096` pro Anfrage laufen sie zu 100 % auf der GPU (0,5–1,5 s).
|
||||
|
||||
**Modellvergleich** (2026-09-14, 11 Fälle: 5 Whisper-Fehler, 5 korrekte Sätze, 1 „Anweisung“; RTX 3090,
|
||||
Prompt mit Tags und Beispielen):
|
||||
|
||||
| Modell | bestanden | Latenz warm | Beobachtung |
|
||||
|---|---|---|---|
|
||||
| `gemma4:12b` | 9/11 | ~1,2 s | widersteht der Anweisung schon per Prompt; scheitert nur an stark verstümmelten Sätzen |
|
||||
| `mistral-nemo` | 9/11 | ~0,6 s | Anweisung und Tags nur durch `_accept` abgefangen; erfindet Namen („Meredith-Stehle“) |
|
||||
| `qwen3.5:9b` | 4/11 | ~0,8 s | verändert Befehle („Schalte das Licht aus“ → „Das Licht ist ausgeschaltet“) |
|
||||
|
||||
Das erste Laden eines Modells dauert 35–75 s; `Output.warmup()` stößt es im Mikrofon-Modus sofort an.
|
||||
|
||||
## 8. Nebenläufigkeit — die wichtigsten Regeln
|
||||
|
||||
1. **Nie** `engine.transcribe` (oder anderes Blockierendes) direkt in einer `async`-Funktion aufrufen —
|
||||
|
|
@ -433,8 +487,8 @@ curl --data-binary @/tmp/t.wav localhost:9998/transcribe | python3 -m json.tool
|
|||
## 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 # 49 Unit-Tests, ~9 s, ohne Modell
|
||||
.venv/bin/python -m pytest -m integration # 5 Integrationstests: Whisper small auf CPU (~20 s), LLM über Ollama (bis ~75 s)
|
||||
.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
|
||||
```
|
||||
|
|
@ -464,6 +518,7 @@ belegt.
|
|||
| 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 |
|
||||
| LLM-Korrektur | `ScriptedCorrector` überschreibt `_chat` mit vorgegebenen Antworten (Annahme, Verwerfen, Fehler, Stückelung); `test_real_llm_correction` nur mit laufendem Ollama (`STT_TEST_LLM_MODEL`) |
|
||||
|
||||
Neue Funktionen bitte mit Unit-Test ohne Modell absichern; nur was wirklich Whisper braucht, als
|
||||
`@pytest.mark.integration`.
|
||||
|
|
@ -513,6 +568,7 @@ für Datei-Upload und WebSocket-Streaming.
|
|||
| 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) |
|
||||
| LLM-Korrektur nur optional in der CLI, mit Ähnlichkeitsprüfung | LLMs können Inhalte verfälschen und kosten ~1 s; Rohtext muss erhalten bleiben | LLM immer im Dienst (legacy `stt.py`): langsam, unkontrolliert, schnitt bei 500 Tokens ab |
|
||||
| Keine Authentifizierung (vorerst) | Heimnetz-Betrieb, einfache Clients (ESP32) | Token: siehe nächste Schritte |
|
||||
|
||||
---
|
||||
|
|
|
|||
Loading…
Reference in a new issue