view 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
line wrap: on
line source

from __future__ import annotations

import argparse
import asyncio
import json
import os
from pathlib import Path
import sys

from dictation.config import (
    DictationConfig,
    MODEL_REPOSITORY,
    MODEL_REVISION,
)


def download_model(config: DictationConfig) -> None:
    from huggingface_hub import snapshot_download

    config.model_dir.mkdir(parents=True, exist_ok=True)
    snapshot_download(
        repo_id=MODEL_REPOSITORY,
        revision=MODEL_REVISION,
        local_dir=config.model_dir,
        allow_patterns=[
            "config.json",
            "model.bin",
            "tokenizer.json",
            "vocabulary.txt",
        ],
    )
    marker = config.model_dir / ".model-revision"
    marker.write_text(f"{MODEL_REPOSITORY}@{MODEL_REVISION}\n", encoding="ascii")
    print(f"Model ready: {config.model_dir}")


def preflight(config: DictationConfig) -> None:
    import ctranslate2

    compute_types = sorted(ctranslate2.get_supported_compute_types("cuda"))
    if config.compute_type not in compute_types:
        raise RuntimeError(
            f"{config.compute_type} is unsupported on CUDA; "
            f"available: {', '.join(compute_types)}"
        )
    result = {
        "cudaDeviceCount": ctranslate2.get_cuda_device_count(),
        "computeTypes": compute_types,
        "selectedComputeType": config.compute_type,
        "modelDir": str(config.model_dir),
        "modelPresent": (config.model_dir / "model.bin").is_file(),
    }
    if result["cudaDeviceCount"] < 1:
        raise RuntimeError("CTranslate2 did not detect a CUDA device")
    print(json.dumps(result, indent=2))


async def transcribe_file(config: DictationConfig, path: Path) -> None:
    from dictation.transcriber import FasterWhisperTranscriber

    transcriber = FasterWhisperTranscriber(
        config.model_dir,
        config.compute_type,
    )
    try:
        from av import open as av_open
        from av.audio.resampler import AudioResampler
        import numpy as np

        samples = []
        resampler = AudioResampler(format="s16", layout="mono", rate=16000)
        with av_open(str(path)) as container:
            for frame in container.decode(audio=0):
                for converted in resampler.resample(frame):
                    samples.append(converted.to_ndarray().reshape(-1))
        if not samples:
            raise RuntimeError("Audio file contains no decodable samples")
        audio = np.concatenate(samples).astype(np.float32) / 32768.0
        result = await transcriber.transcribe(audio, final=True)
        print(
            json.dumps(
                {
                    "text": result.text,
                    "language": result.language,
                    "probability": result.probability,
                },
                ensure_ascii=False,
            )
        )
    finally:
        transcriber.close()


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "command",
        choices=["server", "preflight", "download_model", "transcribe"],
    )
    parser.add_argument("path", nargs="?")
    args = parser.parse_args()
    try:
        config = DictationConfig.from_environment()
        if args.command == "download_model":
            download_model(config)
        elif args.command == "preflight":
            preflight(config)
        elif args.command == "transcribe":
            if not args.path:
                parser.error("transcribe requires an audio file path")
            asyncio.run(transcribe_file(config, Path(args.path)))
        else:
            import uvicorn
            from dictation.server import create_app

            uvicorn.run(
                create_app(config),
                host=config.host,
                port=config.port,
                access_log=False,
            )
    except Exception as error:
        print(f"dictation: {error}", file=sys.stderr)
        raise SystemExit(1) from error


if __name__ == "__main__":
    main()