diff dictation/transcriber.py @ 275:78699f810817

Add Qwen3-VL and WebRTC dictation services Add Bazel targets for the CUDA-backed Qwen3-VL server and a local WebRTC faster-whisper dictation service. Co-authored-by: Copilot <[email protected]> Copilot-Session: e3d8cb06-6c95-4ae0-9757-651d3796ab00
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 10:58:47 -0700
parents
children 49e9e591c9bb
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/dictation/transcriber.py	Mon Aug 17 10:58:47 2026 -0700
@@ -0,0 +1,105 @@
+from __future__ import annotations
+
+import asyncio
+from concurrent.futures import ThreadPoolExecutor
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Protocol
+
+import numpy as np
+
+
+@dataclass(frozen=True)
+class Transcript:
+    text: str
+    language: str
+    probability: float
+
+
+class Transcriber(Protocol):
+    async def warmup(self) -> None:
+        ...
+
+    async def transcribe(
+        self,
+        samples: np.ndarray,
+        *,
+        final: bool,
+    ) -> Transcript:
+        ...
+
+
+class FasterWhisperTranscriber:
+    def __init__(self, model_dir: Path, compute_type: str) -> None:
+        self._model_dir = model_dir
+        self._compute_type = compute_type
+        self._executor = ThreadPoolExecutor(
+            max_workers=1,
+            thread_name_prefix="dictation",
+        )
+        self._model = None
+        self._load_lock = asyncio.Lock()
+
+    async def warmup(self) -> None:
+        if self._model is not None:
+            return
+        async with self._load_lock:
+            if self._model is not None:
+                return
+            loop = asyncio.get_running_loop()
+            self._model = await loop.run_in_executor(
+                self._executor,
+                self._load,
+            )
+
+    def _load(self):
+        from faster_whisper import WhisperModel
+
+        if not (self._model_dir / "model.bin").is_file():
+            raise FileNotFoundError(
+                f"Model not found at {self._model_dir}. "
+                "Run: bazel run //dictation:download_model"
+            )
+        return WhisperModel(
+            str(self._model_dir),
+            device="cuda",
+            compute_type=self._compute_type,
+            local_files_only=True,
+        )
+
+    async def transcribe(
+        self,
+        samples: np.ndarray,
+        *,
+        final: bool,
+    ) -> Transcript:
+        await self.warmup()
+        loop = asyncio.get_running_loop()
+        return await loop.run_in_executor(
+            self._executor,
+            self._transcribe_sync,
+            np.asarray(samples, dtype=np.float32),
+            final,
+        )
+
+    def _transcribe_sync(
+        self,
+        samples: np.ndarray,
+        final: bool,
+    ) -> Transcript:
+        segments, info = self._model.transcribe(
+            samples,
+            beam_size=5 if final else 1,
+            best_of=5 if final else 1,
+            condition_on_previous_text=False,
+            vad_filter=False,
+        )
+        text = "".join(segment.text for segment in segments).strip()
+        return Transcript(
+            text=text,
+            language=info.language or "",
+            probability=float(info.language_probability or 0.0),
+        )
+
+    def close(self) -> None:
+        self._executor.shutdown(wait=False, cancel_futures=True)