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)