121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
"""Wyoming-Protokoll-Adapter für Home Assistant.
|
|
|
|
Läuft im selben Prozess wie stt_service.py und teilt sich Engine + Worker (ein
|
|
Modell im Speicher). In Home Assistant: Einstellungen → Geräte & Dienste →
|
|
Integration „Wyoming Protocol“ → Host dieses Rechners, Port 10300.
|
|
|
|
Ablauf (HA → Server): transcribe → audio-start → audio-chunk* → audio-stop
|
|
Antwort: transcript. Das Äußerungsende bestimmt HA bzw. der Satellit (ESPHome)
|
|
selbst (requires_external_vad), wir transkribieren das komplette Audio.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from functools import partial
|
|
|
|
from wyoming.asr import Transcribe, Transcript
|
|
from wyoming.audio import AudioChunk, AudioChunkConverter, AudioStart, AudioStop
|
|
from wyoming.event import Event
|
|
from wyoming.info import AsrModel, AsrProgram, Attribution, Describe, Info
|
|
from wyoming.server import AsyncEventHandler, AsyncTcpServer
|
|
|
|
import stt_core as core
|
|
|
|
log = logging.getLogger("stt.wyoming")
|
|
|
|
PROJECT_URL = "https://kitux.de/forgejo/dschlueter/whisper-stt-cli"
|
|
|
|
|
|
def _whisper_languages() -> list[str]:
|
|
try:
|
|
from faster_whisper.tokenizer import _LANGUAGE_CODES
|
|
|
|
return sorted(_LANGUAGE_CODES)
|
|
except ImportError:
|
|
return ["de", "en"]
|
|
|
|
|
|
def build_info(engine: core.SttEngine) -> Info:
|
|
model_name = engine.active_model or engine.config.model
|
|
return Info(
|
|
asr=[
|
|
AsrProgram(
|
|
name="speech-to-text",
|
|
description="Lokaler STT-Dienst (faster-whisper)",
|
|
attribution=Attribution(name="dschlueter", url=PROJECT_URL),
|
|
installed=True,
|
|
version=core.__version__,
|
|
models=[
|
|
AsrModel(
|
|
name=model_name,
|
|
description=f"Whisper {model_name}",
|
|
attribution=Attribution(
|
|
name="OpenAI / SYSTRAN", url="https://github.com/SYSTRAN/faster-whisper"
|
|
),
|
|
installed=True,
|
|
version=None,
|
|
languages=_whisper_languages(),
|
|
)
|
|
],
|
|
)
|
|
]
|
|
)
|
|
|
|
|
|
class SttEventHandler(AsyncEventHandler):
|
|
def __init__(self, worker, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.worker = worker
|
|
self._converter = AudioChunkConverter(rate=core.SAMPLE_RATE, width=2, channels=1)
|
|
self._reset()
|
|
|
|
def _reset(self) -> None:
|
|
self._audio = bytearray()
|
|
self._language: str | None = None
|
|
self._hotwords: str | None = None
|
|
|
|
async def handle_event(self, event: Event) -> bool:
|
|
if Describe.is_type(event.type):
|
|
await self.write_event(build_info(self.worker.engine).event())
|
|
return True
|
|
|
|
if Transcribe.is_type(event.type):
|
|
req = Transcribe.from_event(event)
|
|
self._language = req.language
|
|
terms = (req.transcript_names or []) + (req.transcript_terms or [])
|
|
self._hotwords = " ".join(terms) or None
|
|
return True
|
|
|
|
if AudioStart.is_type(event.type):
|
|
self._audio = bytearray()
|
|
self._converter = AudioChunkConverter(rate=core.SAMPLE_RATE, width=2, channels=1)
|
|
return True
|
|
|
|
if AudioChunk.is_type(event.type):
|
|
chunk = self._converter.convert(AudioChunk.from_event(event))
|
|
self._audio.extend(chunk.audio)
|
|
return True
|
|
|
|
if AudioStop.is_type(event.type):
|
|
audio = core.pcm16_to_float(bytes(self._audio))
|
|
text, language = "", self._language
|
|
try:
|
|
tr = await self.worker.transcribe(audio, language=self._language,
|
|
hotwords=self._hotwords)
|
|
text, language = tr.text, tr.language
|
|
log.info("Wyoming-Transkript (%.1fs Audio, %.2fs): %s",
|
|
tr.duration, tr.processing_s, text)
|
|
except Exception as exc: # HA erwartet immer eine Antwort
|
|
log.error("Wyoming-Transkription fehlgeschlagen: %s", exc)
|
|
await self.write_event(Transcript(text=text, language=language).event())
|
|
self._reset()
|
|
return False # HA öffnet pro Anfrage eine neue Verbindung
|
|
|
|
return True
|
|
|
|
|
|
async def start_server(worker, host: str, port: int) -> AsyncTcpServer:
|
|
server = AsyncTcpServer(host, port)
|
|
await server.start(partial(SttEventHandler, worker))
|
|
return server
|