Mercurial
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 274:c9be578316a6 | 275:78699f810817 |
|---|---|
| 1 from __future__ import annotations | |
| 2 | |
| 3 import asyncio | |
| 4 from concurrent.futures import ThreadPoolExecutor | |
| 5 from dataclasses import dataclass | |
| 6 from pathlib import Path | |
| 7 from typing import Protocol | |
| 8 | |
| 9 import numpy as np | |
| 10 | |
| 11 | |
| 12 @dataclass(frozen=True) | |
| 13 class Transcript: | |
| 14 text: str | |
| 15 language: str | |
| 16 probability: float | |
| 17 | |
| 18 | |
| 19 class Transcriber(Protocol): | |
| 20 async def warmup(self) -> None: | |
| 21 ... | |
| 22 | |
| 23 async def transcribe( | |
| 24 self, | |
| 25 samples: np.ndarray, | |
| 26 *, | |
| 27 final: bool, | |
| 28 ) -> Transcript: | |
| 29 ... | |
| 30 | |
| 31 | |
| 32 class FasterWhisperTranscriber: | |
| 33 def __init__(self, model_dir: Path, compute_type: str) -> None: | |
| 34 self._model_dir = model_dir | |
| 35 self._compute_type = compute_type | |
| 36 self._executor = ThreadPoolExecutor( | |
| 37 max_workers=1, | |
| 38 thread_name_prefix="dictation", | |
| 39 ) | |
| 40 self._model = None | |
| 41 self._load_lock = asyncio.Lock() | |
| 42 | |
| 43 async def warmup(self) -> None: | |
| 44 if self._model is not None: | |
| 45 return | |
| 46 async with self._load_lock: | |
| 47 if self._model is not None: | |
| 48 return | |
| 49 loop = asyncio.get_running_loop() | |
| 50 self._model = await loop.run_in_executor( | |
| 51 self._executor, | |
| 52 self._load, | |
| 53 ) | |
| 54 | |
| 55 def _load(self): | |
| 56 from faster_whisper import WhisperModel | |
| 57 | |
| 58 if not (self._model_dir / "model.bin").is_file(): | |
| 59 raise FileNotFoundError( | |
| 60 f"Model not found at {self._model_dir}. " | |
| 61 "Run: bazel run //dictation:download_model" | |
| 62 ) | |
| 63 return WhisperModel( | |
| 64 str(self._model_dir), | |
| 65 device="cuda", | |
| 66 compute_type=self._compute_type, | |
| 67 local_files_only=True, | |
| 68 ) | |
| 69 | |
| 70 async def transcribe( | |
| 71 self, | |
| 72 samples: np.ndarray, | |
| 73 *, | |
| 74 final: bool, | |
| 75 ) -> Transcript: | |
| 76 await self.warmup() | |
| 77 loop = asyncio.get_running_loop() | |
| 78 return await loop.run_in_executor( | |
| 79 self._executor, | |
| 80 self._transcribe_sync, | |
| 81 np.asarray(samples, dtype=np.float32), | |
| 82 final, | |
| 83 ) | |
| 84 | |
| 85 def _transcribe_sync( | |
| 86 self, | |
| 87 samples: np.ndarray, | |
| 88 final: bool, | |
| 89 ) -> Transcript: | |
| 90 segments, info = self._model.transcribe( | |
| 91 samples, | |
| 92 beam_size=5 if final else 1, | |
| 93 best_of=5 if final else 1, | |
| 94 condition_on_previous_text=False, | |
| 95 vad_filter=False, | |
| 96 ) | |
| 97 text = "".join(segment.text for segment in segments).strip() | |
| 98 return Transcript( | |
| 99 text=text, | |
| 100 language=info.language or "", | |
| 101 probability=float(info.language_probability or 0.0), | |
| 102 ) | |
| 103 | |
| 104 def close(self) -> None: | |
| 105 self._executor.shutdown(wait=False, cancel_futures=True) |