Spaces:
Sleeping
Sleeping
| """Pinned, resumable asset setup for ResearchMate's pocket-tts default voice and Piper fallback voice.""" | |
| from __future__ import annotations | |
| import argparse | |
| import hashlib | |
| import json | |
| from dataclasses import asdict, dataclass | |
| from pathlib import Path | |
| class DownloadSpec: | |
| name: str | |
| repo_id: str | |
| revision: str | |
| destination: str | |
| allow_patterns: tuple[str, ...] = () | |
| def download_specs() -> list[DownloadSpec]: | |
| return [ | |
| DownloadSpec( | |
| "pocket-tts", | |
| "kyutai/pocket-tts", | |
| "4c8ad48f8a003909bc4f1122cbe88a4252124621", | |
| "pocket", | |
| ), | |
| DownloadSpec( | |
| "piper-en-us", | |
| "rhasspy/piper-voices", | |
| "e21c7de8d4eab79b902f0d61e662b3f21664b8d2", | |
| "piper", | |
| ( | |
| "en/en_US/lessac/medium/en_US-lessac-medium.onnx", | |
| "en/en_US/lessac/medium/en_US-lessac-medium.onnx.json", | |
| "en/en_US/lessac/medium/MODEL_CARD", | |
| ), | |
| ), | |
| ] | |
| def setup_assets(root: Path) -> dict: | |
| from huggingface_hub import snapshot_download | |
| root.mkdir(parents=True, exist_ok=True) | |
| rows: list[dict] = [] | |
| for spec in download_specs(): | |
| destination = root / spec.destination | |
| try: | |
| snapshot_download( | |
| repo_id=spec.repo_id, | |
| revision=spec.revision, | |
| allow_patterns=list(spec.allow_patterns) or None, | |
| local_dir=destination, | |
| ) | |
| rows.append({ | |
| **asdict(spec), | |
| "status": "installed", | |
| "bytes": _directory_size(destination), | |
| "checksums": _checksums(destination), | |
| }) | |
| except Exception as exc: | |
| rows.append({**asdict(spec), "status": "error", "error": str(exc)}) | |
| manifest = {"schema_version": 1, "root": str(root), "assets": rows, "total_bytes": _directory_size(root)} | |
| (root / "installation-manifest.json").write_text(json.dumps(manifest, indent=2), encoding="utf-8") | |
| return manifest | |
| def _directory_size(path: Path) -> int: | |
| return sum(item.stat().st_size for item in path.rglob("*") if item.is_file()) if path.exists() else 0 | |
| 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 _checksums(path: Path) -> dict[str, str]: | |
| return { | |
| item.relative_to(path).as_posix(): _sha256(item) | |
| for item in path.rglob("*") if item.is_file() and not item.name.startswith(".cache") | |
| } | |
| def main() -> None: | |
| parser = argparse.ArgumentParser(description="Download pinned Pocket TTS and Piper assets.") | |
| parser.add_argument("--root", type=Path, default=Path.home() / ".studybuddy" / "models") | |
| parser.add_argument("--yes", action="store_true", help="Accept the model download without prompting") | |
| args = parser.parse_args() | |
| if not args.yes: | |
| answer = input("Download Pocket TTS and Piper assets? [y/N] ") | |
| if answer.strip().lower() not in {"y", "yes"}: | |
| return | |
| manifest = setup_assets(args.root) | |
| print(json.dumps(manifest, indent=2)) | |
| if __name__ == "__main__": | |
| main() | |