feat(stt): echtes lokales STT via faster-whisper (optional .[local])
- FasterWhisperProvider implementiert (CTranslate2): Modell prozessweit gecacht (lru_cache), Transkription in asyncio.to_thread; robuster CPU-Fallback wenn GPU/compute_type nicht verfuegbar - Config: FASTER_WHISPER_MODEL/DEVICE/COMPUTE_TYPE (Defaults base/auto/default) - pyproject: optionales Extra [local] = faster-whisper - Test: transcribe-Endpunkt nutzt jetzt einen Stub-STT (kein Platzhalter mehr) - Doku: Voraussetzungen, Hybrid-Beispiel (STT+LLM lokal, TTS remote), Architektur/README Live verifiziert: TTS->WAV->faster-whisper transkribiert korrekt (de); voller Hybrid ueber /ws/voice (faster-whisper + Ollama llama3.2 + OpenRouter-TTS) funktioniert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e1167951e1
commit
c25f081f9f
8 changed files with 92 additions and 8 deletions
|
|
@ -37,6 +37,11 @@ LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1
|
||||||
LOCAL_LLM_API_KEY=dummy
|
LOCAL_LLM_API_KEY=dummy
|
||||||
LOCAL_LLM_MODEL=llama3.1
|
LOCAL_LLM_MODEL=llama3.1
|
||||||
|
|
||||||
|
# --- Lokales STT (faster-whisper; nur mit pip install -e .[local] ) ---------
|
||||||
|
FASTER_WHISPER_MODEL=base # tiny|base|small|medium|large-v3
|
||||||
|
FASTER_WHISPER_DEVICE=auto # auto|cpu|cuda
|
||||||
|
FASTER_WHISPER_COMPUTE_TYPE=default # default|int8|float16|int8_float16
|
||||||
|
|
||||||
# --- Resilienz: Fallback-Ketten (kommaseparierte Provider-Namen) ------------
|
# --- Resilienz: Fallback-Ketten (kommaseparierte Provider-Namen) ------------
|
||||||
# Faellt der primaere Provider aus, uebernimmt der naechste.
|
# Faellt der primaere Provider aus, uebernimmt der naechste.
|
||||||
# STT_FALLBACK=faster-whisper
|
# STT_FALLBACK=faster-whisper
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ Kurzüberblick: [README](README.md).
|
||||||
(`ffplay`/`aplay`/`paplay`) — auf den meisten Linux-Desktops vorhanden
|
(`ffplay`/`aplay`/`paplay`) — auf den meisten Linux-Desktops vorhanden
|
||||||
- **OpenRouter-API-Key** — nötig für Profile mit Cloud-KI (`hybrid`, `cloud`);
|
- **OpenRouter-API-Key** — nötig für Profile mit Cloud-KI (`hybrid`, `cloud`);
|
||||||
für rein lokalen Betrieb (`local-dev`) nicht
|
für rein lokalen Betrieb (`local-dev`) nicht
|
||||||
|
- Für **lokales STT** (Provider `faster-whisper`): einmalig `pip install -e .[local]`
|
||||||
|
(lädt beim ersten Lauf ein Whisper-Modell). Für **lokales LLM**: ein laufender
|
||||||
|
Ollama-Server (`http://127.0.0.1:11434`) mit einem Modell (`ollama pull llama3.2`)
|
||||||
- Optional: Docker
|
- Optional: Docker
|
||||||
|
|
||||||
## 2. Installation
|
## 2. Installation
|
||||||
|
|
@ -213,6 +216,22 @@ curl -s -X POST $URL/api/sessions/oma-anna/route \
|
||||||
|
|
||||||
Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
|
Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
|
||||||
|
|
||||||
|
**Hybrid-Beispiel** (Aufnahme + STT + LLM **lokal**, nur TTS **remote**):
|
||||||
|
```bash
|
||||||
|
# einmalig: lokales STT installieren
|
||||||
|
pip install -e .[local]
|
||||||
|
# Server mit kleinem lokalem Ollama-Modell (muss in 'ollama list' stehen):
|
||||||
|
echo 'LOCAL_LLM_MODEL=llama3.2:latest' >> .env
|
||||||
|
make run
|
||||||
|
# in Terminal 2 — Sprech-Loop mit der Hybrid-Kombi (MOTU = plughw:5,0):
|
||||||
|
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hybrid \
|
||||||
|
--stt-provider faster-whisper \
|
||||||
|
--llm-provider local-openai-compatible \
|
||||||
|
--tts-provider openrouter
|
||||||
|
```
|
||||||
|
Erster Turn ist langsamer (Whisper- und Ollama-Modell laden), danach zügig. STT-Modell
|
||||||
|
und Gerät steuern `FASTER_WHISPER_MODEL`/`FASTER_WHISPER_DEVICE` in `.env`.
|
||||||
|
|
||||||
## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)
|
## B2. Soundquelle & Ausgabe-Gerät wechseln (Mikrofon, Lautsprecher, Bluetooth, Handy)
|
||||||
|
|
||||||
> **Wichtig — aktueller Stand:** Die Geräte-Endpunkte **im Gateway**
|
> **Wichtig — aktueller Stand:** Die Geräte-Endpunkte **im Gateway**
|
||||||
|
|
|
||||||
|
|
@ -195,10 +195,14 @@ erkennung und Barge-in (`interrupt`)**; **Resilienz (Fallback-Ketten je Modul,
|
||||||
In-Memory-Metriken `/api/metrics`), Tageskontingent pro Nutzer und heuristische
|
In-Memory-Metriken `/api/metrics`), Tageskontingent pro Nutzer und heuristische
|
||||||
Notfall-Eskalation**; automatisierte Tests.
|
Notfall-Eskalation**; automatisierte Tests.
|
||||||
|
|
||||||
|
**Echtes lokales STT:** `faster-whisper` (optionale Dependency `.[local]`,
|
||||||
|
CTranslate2) transkribiert real — damit ist ein Hybrid „STT+LLM lokal, TTS remote"
|
||||||
|
möglich (live verifiziert).
|
||||||
|
|
||||||
**Platzhalter (Gerüst):** Audio-Endpunkte (`local-default`, `bluetooth`,
|
**Platzhalter (Gerüst):** Audio-Endpunkte (`local-default`, `bluetooth`,
|
||||||
`mobile-ws`, `mobile-webrtc`) liefern leere Chunks — nur Auswahl/Lifecycle sind
|
`mobile-ws`, `mobile-webrtc`) liefern leere Chunks — nur Auswahl/Lifecycle sind
|
||||||
verdrahtet, kein echtes Hardware-I/O. Lokale Provider `faster-whisper`, `piper`,
|
verdrahtet, kein echtes Hardware-I/O. Lokale TTS-Provider `piper`, `chatterbox`
|
||||||
`chatterbox` sind Stubs. `transport_router.py` (Ebene 4) existiert, ist aber noch
|
sind noch Stubs. `transport_router.py` (Ebene 4) existiert, ist aber noch
|
||||||
nicht aktiv (lokal/remote trägt vorerst der Provider-Name).
|
nicht aktiv (lokal/remote trägt vorerst der Provider-Name).
|
||||||
|
|
||||||
## 9. Roadmap / bewusste nächste Schritte
|
## 9. Roadmap / bewusste nächste Schritte
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ Praktische Bedienung: [`BEDIENUNGSANLEITUNG.md`](BEDIENUNGSANLEITUNG.md).
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Pipeline mit getrennter Semantik/Sprache:** STT → Input-Cleaner → LLM → Spoken-Adapter → TTS-Normalizer → TTS
|
- **Pipeline mit getrennter Semantik/Sprache:** STT → Input-Cleaner → LLM → Spoken-Adapter → TTS-Normalizer → TTS
|
||||||
- **Provider austauschbar** über Registry (OpenRouter remote; faster-whisper/piper/chatterbox als lokale Stubs)
|
- **Provider austauschbar** über Registry (OpenRouter remote; lokales STT via faster-whisper `.[local]`; piper/chatterbox-TTS noch Stubs)
|
||||||
- **Geschichtete Konfiguration** mit Profilen (`local-dev` / `hybrid` / `cloud`)
|
- **Geschichtete Konfiguration** mit Profilen (`local-dev` / `hybrid` / `cloud`)
|
||||||
- **Routing auf jeder Ebene:** Default → Profil → Nutzer → Session → Request
|
- **Routing auf jeder Ebene:** Default → Profil → Nutzer → Session → Request
|
||||||
- **Authentifizierung** (Bearer-Token) + persistente Nutzer/Sessions (SQLite)
|
- **Authentifizierung** (Bearer-Token) + persistente Nutzer/Sessions (SQLite)
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,9 @@ class Settings(BaseSettings):
|
||||||
local_llm_base_url: str = "http://127.0.0.1:11434/v1"
|
local_llm_base_url: str = "http://127.0.0.1:11434/v1"
|
||||||
local_llm_api_key: str = "dummy"
|
local_llm_api_key: str = "dummy"
|
||||||
local_llm_model: str = "llama3.1"
|
local_llm_model: str = "llama3.1"
|
||||||
|
faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3
|
||||||
|
faster_whisper_device: str = "auto" # auto|cpu|cuda
|
||||||
|
faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16
|
||||||
db_path: str = str(BASE_DIR / "data" / "voice-assistant.db")
|
db_path: str = str(BASE_DIR / "data" / "voice-assistant.db")
|
||||||
admin_api_key: str = ""
|
admin_api_key: str = ""
|
||||||
auth_enabled: bool = True
|
auth_enabled: bool = True
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,49 @@
|
||||||
|
"""Lokaler STT-Provider auf Basis von faster-whisper (CTranslate2).
|
||||||
|
|
||||||
|
Optionale Dependency: `pip install -e .[local]`. Das Whisper-Modell wird beim
|
||||||
|
ersten Aufruf geladen (und ggf. heruntergeladen) und prozessweit zwischengespeichert.
|
||||||
|
Die Transkription ist CPU/GPU-lastig und laeuft daher in einem Thread, damit der
|
||||||
|
Event-Loop frei bleibt.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import io
|
||||||
|
from functools import lru_cache
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
from app.providers.stt.base import STTProvider
|
from app.providers.stt.base import STTProvider
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=2)
|
||||||
|
def _load_model(model_size: str, device: str, compute_type: str):
|
||||||
|
try:
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
except ModuleNotFoundError as exc: # pragma: no cover - haengt von Installation ab
|
||||||
|
raise RuntimeError(
|
||||||
|
"faster-whisper ist nicht installiert. Installieren mit: pip install -e .[local]"
|
||||||
|
) from exc
|
||||||
|
try:
|
||||||
|
return WhisperModel(model_size, device=device, compute_type=compute_type)
|
||||||
|
except Exception:
|
||||||
|
# GPU/Compute-Type nicht verfuegbar -> robuster CPU-Fallback (int8).
|
||||||
|
if device != "cpu":
|
||||||
|
return WhisperModel(model_size, device="cpu", compute_type="int8")
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class FasterWhisperProvider(STTProvider):
|
class FasterWhisperProvider(STTProvider):
|
||||||
|
def __init__(self, model_size: str | None = None, device: str | None = None,
|
||||||
|
compute_type: str | None = None):
|
||||||
|
self.model_size = model_size or settings.faster_whisper_model
|
||||||
|
self.device = device or settings.faster_whisper_device
|
||||||
|
self.compute_type = compute_type or settings.faster_whisper_compute_type
|
||||||
|
|
||||||
|
def _transcribe_sync(self, audio_bytes: bytes, language: str | None) -> str:
|
||||||
|
model = _load_model(self.model_size, self.device, self.compute_type)
|
||||||
|
segments, _info = model.transcribe(io.BytesIO(audio_bytes), language=language)
|
||||||
|
return "".join(segment.text for segment in segments).strip()
|
||||||
|
|
||||||
async def transcribe(self, audio_bytes: bytes, fmt: str, language: str | None = None) -> str:
|
async def transcribe(self, audio_bytes: bytes, fmt: str, language: str | None = None) -> str:
|
||||||
return "[local transcription placeholder]"
|
if not audio_bytes:
|
||||||
|
raise ValueError("STT input audio is empty")
|
||||||
|
return await asyncio.to_thread(self._transcribe_sync, audio_bytes, language)
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ dependencies = [
|
||||||
test = [
|
test = [
|
||||||
"pytest>=8.0"
|
"pytest>=8.0"
|
||||||
]
|
]
|
||||||
|
# Lokale KI-Module (optional, schwergewichtig): lokales STT via faster-whisper.
|
||||||
|
local = [
|
||||||
|
"faster-whisper>=1.0"
|
||||||
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=68", "wheel"]
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
|
|
||||||
|
|
@ -75,14 +75,19 @@ def test_chat_per_request_override_and_loopback(monkeypatch):
|
||||||
assert loopback_output().chunks[0].data == b"AUDIO"
|
assert loopback_output().chunks[0].data == b"AUDIO"
|
||||||
|
|
||||||
|
|
||||||
def test_transcribe_local_provider():
|
def test_transcribe_with_stub_stt(monkeypatch):
|
||||||
|
class StubSTT:
|
||||||
|
async def transcribe(self, audio_bytes, fmt, language=None):
|
||||||
|
return "erkannter text"
|
||||||
|
|
||||||
|
monkeypatch.setitem(deps.STT_REGISTRY, "stub-stt", lambda s: StubSTT())
|
||||||
files = {"file": ("a.wav", b"RIFFdata", "audio/wav")}
|
files = {"file": ("a.wav", b"RIFFdata", "audio/wav")}
|
||||||
data = {"stt_provider": "faster-whisper"}
|
data = {"stt_provider": "stub-stt"}
|
||||||
resp = client.post("/api/transcribe", data=data, files=files)
|
resp = client.post("/api/transcribe", data=data, files=files)
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
body = resp.json()
|
body = resp.json()
|
||||||
assert body["route"]["stt_provider"] == "faster-whisper"
|
assert body["route"]["stt_provider"] == "stub-stt"
|
||||||
assert body["trace"]["raw_transcript"] == "[local transcription placeholder]"
|
assert body["trace"]["raw_transcript"] == "erkannter text"
|
||||||
|
|
||||||
|
|
||||||
def test_config_endpoint_exposes_no_secrets():
|
def test_config_endpoint_exposes_no_secrets():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue