comparison dictation/main.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
comparison
equal deleted inserted replaced
274:c9be578316a6 275:78699f810817
1 from __future__ import annotations
2
3 import argparse
4 import asyncio
5 import json
6 import os
7 from pathlib import Path
8 import sys
9
10 from dictation.config import (
11 DictationConfig,
12 MODEL_REPOSITORY,
13 MODEL_REVISION,
14 )
15
16
17 def download_model(config: DictationConfig) -> None:
18 from huggingface_hub import snapshot_download
19
20 config.model_dir.mkdir(parents=True, exist_ok=True)
21 snapshot_download(
22 repo_id=MODEL_REPOSITORY,
23 revision=MODEL_REVISION,
24 local_dir=config.model_dir,
25 allow_patterns=[
26 "config.json",
27 "model.bin",
28 "tokenizer.json",
29 "vocabulary.txt",
30 ],
31 )
32 marker = config.model_dir / ".model-revision"
33 marker.write_text(f"{MODEL_REPOSITORY}@{MODEL_REVISION}\n", encoding="ascii")
34 print(f"Model ready: {config.model_dir}")
35
36
37 def preflight(config: DictationConfig) -> None:
38 import ctranslate2
39
40 compute_types = sorted(ctranslate2.get_supported_compute_types("cuda"))
41 if config.compute_type not in compute_types:
42 raise RuntimeError(
43 f"{config.compute_type} is unsupported on CUDA; "
44 f"available: {', '.join(compute_types)}"
45 )
46 result = {
47 "cudaDeviceCount": ctranslate2.get_cuda_device_count(),
48 "computeTypes": compute_types,
49 "selectedComputeType": config.compute_type,
50 "modelDir": str(config.model_dir),
51 "modelPresent": (config.model_dir / "model.bin").is_file(),
52 }
53 if result["cudaDeviceCount"] < 1:
54 raise RuntimeError("CTranslate2 did not detect a CUDA device")
55 print(json.dumps(result, indent=2))
56
57
58 async def transcribe_file(config: DictationConfig, path: Path) -> None:
59 from dictation.transcriber import FasterWhisperTranscriber
60
61 transcriber = FasterWhisperTranscriber(
62 config.model_dir,
63 config.compute_type,
64 )
65 try:
66 from av import open as av_open
67 from av.audio.resampler import AudioResampler
68 import numpy as np
69
70 samples = []
71 resampler = AudioResampler(format="s16", layout="mono", rate=16000)
72 with av_open(str(path)) as container:
73 for frame in container.decode(audio=0):
74 for converted in resampler.resample(frame):
75 samples.append(converted.to_ndarray().reshape(-1))
76 if not samples:
77 raise RuntimeError("Audio file contains no decodable samples")
78 audio = np.concatenate(samples).astype(np.float32) / 32768.0
79 result = await transcriber.transcribe(audio, final=True)
80 print(
81 json.dumps(
82 {
83 "text": result.text,
84 "language": result.language,
85 "probability": result.probability,
86 },
87 ensure_ascii=False,
88 )
89 )
90 finally:
91 transcriber.close()
92
93
94 def main() -> None:
95 parser = argparse.ArgumentParser()
96 parser.add_argument(
97 "command",
98 choices=["server", "preflight", "download_model", "transcribe"],
99 )
100 parser.add_argument("path", nargs="?")
101 args = parser.parse_args()
102 try:
103 config = DictationConfig.from_environment()
104 if args.command == "download_model":
105 download_model(config)
106 elif args.command == "preflight":
107 preflight(config)
108 elif args.command == "transcribe":
109 if not args.path:
110 parser.error("transcribe requires an audio file path")
111 asyncio.run(transcribe_file(config, Path(args.path)))
112 else:
113 import uvicorn
114 from dictation.server import create_app
115
116 uvicorn.run(
117 create_app(config),
118 host=config.host,
119 port=config.port,
120 access_log=False,
121 )
122 except Exception as error:
123 print(f"dictation: {error}", file=sys.stderr)
124 raise SystemExit(1) from error
125
126
127 if __name__ == "__main__":
128 main()