File size: 3,711 Bytes
692f0d2
 
13489f2
 
2331911
 
692f0d2
 
 
 
 
726a195
2331911
692f0d2
 
 
 
2331911
692f0d2
 
726a195
 
 
 
 
 
 
 
 
692f0d2
726a195
692f0d2
 
726a195
 
692f0d2
 
 
726a195
 
 
692f0d2
 
2331911
 
 
 
 
 
 
 
 
 
 
692f0d2
2331911
 
692f0d2
 
2331911
 
 
 
 
 
 
726a195
 
2331911
692f0d2
 
 
 
 
 
 
13489f2
692f0d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Download the pinned multilingual Whisper checkpoint into the model cache.

Optional Piper voices are downloaded only with ``--tts``. The file list is
every voice in ``model_registry/voices.json``, hosted on
``ruslanmv/hp-piper-voices``. Files are not loaded unless ``SONIC_TTS_ENABLED``
is true and both the ONNX file and its JSON config are present.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import sys
import urllib.request
from pathlib import Path

VOICE_BASE = "https://huggingface.co/ruslanmv/hp-piper-voices/resolve/main"


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _download(url: str, destination: Path, expected_sha256: str) -> None:
    destination.parent.mkdir(parents=True, exist_ok=True)
    if destination.is_file() and _sha256(destination) == expected_sha256:
        print(f"already present: {destination}")
        return
    if destination.is_file():
        destination.unlink()
    print(f"downloading {url}")
    with urllib.request.urlopen(url, timeout=120) as response:  # noqa: S310 - explicit model URL
        destination.write_bytes(response.read())
    if _sha256(destination) != expected_sha256:
        destination.unlink()
        raise RuntimeError(f"checksum mismatch for {destination.name}")


def _registry_path() -> Path:
    here = Path(__file__).resolve()
    candidates = [Path.cwd() / "model_registry" / "voices.json"]
    if len(here.parents) > 2:
        candidates.append(here.parents[2] / "model_registry" / "voices.json")
    for candidate in candidates:
        if candidate.is_file():
            return candidate
    raise FileNotFoundError("model_registry/voices.json was not found")


def install_voices(cache_dir: Path) -> None:
    payload = json.loads(_registry_path().read_text(encoding="utf-8"))
    base = str((payload.get("source") or {}).get("base_url") or VOICE_BASE).rstrip("/")
    voice_dir = cache_dir / "tts"
    voice_dir.mkdir(parents=True, exist_ok=True)
    seen: set[str] = set()
    for item in payload.get("voices") or []:
        model_path = str(item.get("model_path") or "")
        config_path = str(item.get("config_path") or "")
        if not model_path or model_path in seen:
            continue
        seen.add(model_path)
        _download(f"{base}/{model_path}", voice_dir / Path(model_path).name, str(item.get("model_sha256") or ""))
        _download(f"{base}/{config_path}", voice_dir / Path(config_path).name, str(item.get("config_sha256") or ""))
        print(f"voice ready: {Path(model_path).stem}")


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Install SonicAgent speech models")
    parser.add_argument("--model", default="tiny", help="Multilingual Whisper size. tiny.en is rejected.")
    parser.add_argument("--cache-dir", default="~/.cache/sonicagent-rt")
    parser.add_argument("--revision", default=None)
    parser.add_argument("--tts", action="store_true", help="Also download every Piper voice in the registry")
    args = parser.parse_args(argv)
    cache = Path(args.cache_dir).expanduser()
    from sonicagent_rt.stt.model_store import install_whisper_model

    try:
        path = install_whisper_model(args.model, cache, revision=args.revision)
    except Exception as exc:
        print(f"model install failed: {exc}", file=sys.stderr)
        return 1
    print(f"whisper model ready: {path}")
    if args.tts:
        install_voices(cache)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())