Mercurial
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() |