personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(install): consolidate model install into sol install-models subcommand

Move parakeet installer logic from scripts/install_parakeet_model.py
into think/install_models.py and register as `sol install-models`.
Extend coverage to bundled wespeaker + pyannote .onnx assets via the
existing observe.transcribe.main constants and observe.utils.compute_file_sha256,
removing the inline python -c SHA verifier from the .installed Makefile
recipe. Single source of truth for "ML models present and verified" is
now `sol install-models` (and `sol install-models --check` for verify-only).

Adds a `Setup:` group to `sol --help` (anticipates sibling install
subcommands). Adds a true `--check` (verify only, no fetch, nonzero on
any failure) and a `--variant {auto,cpu,cuda,coreml}` flag with
documented precedence (flag > PARAKEET_ONNX_VARIANT env > nvidia-smi
autodetect > platform default). The env var stays honored for one
release cycle as the auto-mode fallback.

Makefile install/install-models targets delegate to `.venv/bin/sol
install-models`. The PARAKEET_ONNX_VARIANT autodetect at Makefile:27
that drives `uv sync --extra parakeet-onnx-$(VARIANT)` is unrelated
and unchanged.

Tests migrated to direct imports from think.install_models (no more
runpy probe). New coverage: variant-resolution precedence, bundled-asset
SHA mismatch (mutate-byte case), and `--check` exit codes.

scripts/install_parakeet_model.py is deleted with no shim.

+345 -130
+4 -4
INSTALL.md
··· 65 65 66 66 - `device: "auto"` is primarily for Linux; macOS always routes to the CoreML helper on Apple Silicon. 67 67 - `quantization: "auto"` resolves to fp32 on Linux. The old `precision` key is still accepted as a one-release deprecated alias for `quantization`. 68 - - `make install` builds the platform prerequisites and then runs `make install-models`. 69 - - `make install-models` is idempotent and prints `model ready: <path>` when the local model cache is ready. 70 - - **macOS (Apple Silicon):** Xcode command line tools are required because the helper is a Swift package; if the `xcodebuild -version` check above fails, fix that first. `make install-models` downloads roughly 461 MB of model data into `~/Library/Application Support/solstone/parakeet/models`. 71 - - **Linux (x86_64):** `make install` auto-detects `PARAKEET_ONNX_VARIANT` (`cuda` when `nvidia-smi -L` succeeds, otherwise `cpu`), runs `uv sync --extra parakeet-onnx-<variant>`, then runs `make install-models`. Override detection with `PARAKEET_ONNX_VARIANT=cuda make install` or `PARAKEET_ONNX_VARIANT=cpu make install`. The CPU extra installs `parakeet-onnx-cpu`; the CUDA extra installs `parakeet-onnx-cuda`. The ONNX model footprint is roughly 2.55 GB, and the CUDA wheels add roughly 700 MB more when using the CUDA variant. 68 + - `make install` builds the platform prerequisites and then runs `sol install-models`. 69 + - `sol install-models` is idempotent and prints `model ready: <path>` when the local model cache is ready. 70 + - **macOS (Apple Silicon):** Xcode command line tools are required because the helper is a Swift package; if the `xcodebuild -version` check above fails, fix that first. `sol install-models` downloads roughly 461 MB of model data into `~/Library/Application Support/solstone/parakeet/models`. 71 + - **Linux (x86_64):** `make install` auto-detects `PARAKEET_ONNX_VARIANT` (`cuda` when `nvidia-smi -L` succeeds, otherwise `cpu`), runs `uv sync --extra parakeet-onnx-<variant>`, then runs `sol install-models`. Override direct model installation with `sol install-models --variant cuda` or `sol install-models --variant cpu`; `PARAKEET_ONNX_VARIANT` is honored for one release cycle as a fallback when `--variant auto` is used. The CPU extra installs `parakeet-onnx-cpu`; the CUDA extra installs `parakeet-onnx-cuda`. The ONNX model footprint is roughly 2.55 GB, and the CUDA wheels add roughly 700 MB more when using the CUDA variant. 72 72 - To use Whisper instead, set `transcribe.backend = "whisper"` in `journal/config/journal.json` or switch the backend in the settings UI. The Whisper code and dependencies remain available for rollback. 73 73 - helper contract details live in `observe/transcribe/parakeet_helper/README.md`. 74 74
+5 -9
Makefile
··· 67 67 echo "parakeet install: PARAKEET_ONNX_VARIANT=$(PARAKEET_ONNX_VARIANT)"; \ 68 68 $(UV) sync --extra parakeet-onnx-$(PARAKEET_ONNX_VARIANT) || { echo "parakeet install: uv sync --extra parakeet-onnx-$(PARAKEET_ONNX_VARIANT) failed" >&2; exit 1; }; \ 69 69 fi 70 - @$(VENV_BIN)/python -c "from observe.transcribe.main import PYANNOTE_OVERLAP_MODEL_PATH, PYANNOTE_OVERLAP_MODEL_SHA256, WESPEAKER_MODEL_PATH, WESPEAKER_MODEL_SHA256; from observe.utils import compute_file_sha256; actual = compute_file_sha256(WESPEAKER_MODEL_PATH); assert actual == WESPEAKER_MODEL_SHA256, f'WeSpeaker asset hash mismatch: got {actual}, expected {WESPEAKER_MODEL_SHA256}'; print(f'wespeaker asset ok ({actual[:12]}...)'); actual = compute_file_sha256(PYANNOTE_OVERLAP_MODEL_PATH); assert actual == PYANNOTE_OVERLAP_MODEL_SHA256, f'pyannote asset hash mismatch: got {actual}, expected {PYANNOTE_OVERLAP_MODEL_SHA256}'; print(f'pyannote asset ok ({actual[:12]}...)')" 71 70 @$(MAKE) --no-print-directory skills 72 71 @touch .installed 73 72 ··· 107 106 exit 1; \ 108 107 fi 109 108 @touch .installed 110 - @OS_NAME=$$(uname -s); \ 111 - ARCH=$$(uname -m); \ 112 - if [ "$$OS_NAME" = "Darwin" ] && [ "$$ARCH" = "arm64" ] || [ "$$OS_NAME" = "Linux" ] && [ "$$ARCH" = "x86_64" ]; then \ 113 - PARAKEET_ONNX_VARIANT=$(PARAKEET_ONNX_VARIANT) $(VENV_PY) scripts/install_parakeet_model.py || { echo "parakeet install: install_parakeet_model.py failed" >&2; exit 1; }; \ 114 - fi 109 + @$(VENV_BIN)/sol install-models || { echo "sol install-models failed" >&2; exit 1; } 115 110 116 111 # Directories where AI coding agents look for skills 117 112 SKILL_DIRS := journal/.agents/skills journal/.claude/skills ··· 284 279 curl -fsSL https://pinchtab.com/install.sh | bash; \ 285 280 fi 286 281 287 - # Build the parakeet helper binary (macOS/arm64 only, requires Xcode CLT) 282 + # Install and verify local ML models 288 283 install-models: 289 - @test -x "$(VENV_PY)" || { echo "parakeet install: missing $(VENV_PY); run make install first" >&2; exit 1; } 290 - PARAKEET_ONNX_VARIANT=$(PARAKEET_ONNX_VARIANT) $(VENV_PY) scripts/install_parakeet_model.py 284 + @test -x "$(VENV_BIN)/sol" || { echo "missing $(VENV_BIN)/sol; run make install first" >&2; exit 1; } 285 + $(VENV_BIN)/sol install-models 291 286 287 + # Build the parakeet helper binary (macOS/arm64 only, requires Xcode CLT) 292 288 parakeet-helper: 293 289 cd observe/transcribe/parakeet_helper && swift build -c release 294 290 @echo "built: $$(pwd)/observe/transcribe/parakeet_helper/.build/release/parakeet-helper"
+151 -24
scripts/install_parakeet_model.py think/install_models.py
··· 16 16 from pathlib import Path 17 17 from typing import Any 18 18 19 + from observe.transcribe.main import ( 20 + PYANNOTE_OVERLAP_MODEL_PATH, 21 + PYANNOTE_OVERLAP_MODEL_SHA256, 22 + WESPEAKER_MODEL_PATH, 23 + WESPEAKER_MODEL_SHA256, 24 + ) 25 + from observe.utils import compute_file_sha256 26 + 19 27 BACKEND = "parakeet" 20 28 MODEL_VERSION = "v3" 21 29 MODEL_ID = "istupakov/parakeet-tdt-0.6b-v3-onnx" ··· 63 71 return os_name, platform.machine().lower() 64 72 65 73 66 - def _variant_for_platform(os_name: str, arch: str) -> str | None: 74 + def _detect_linux_variant() -> str: 75 + try: 76 + result = subprocess.run( 77 + ["nvidia-smi", "-L"], 78 + check=False, 79 + capture_output=True, 80 + text=True, 81 + ) 82 + except OSError: 83 + return "cpu" 84 + if result.returncode == 0 and result.stdout.strip(): 85 + return "cuda" 86 + return "cpu" 87 + 88 + 89 + def _resolve_variant( 90 + flag_value: str, 91 + env_value: str | None, 92 + os_name: str, 93 + arch: str, 94 + ) -> str | None: 95 + if flag_value in {"cpu", "cuda"}: 96 + if os_name != "linux": 97 + raise SystemExit(f"variant {flag_value!r} not supported on {os_name}") 98 + if arch != "x86_64": 99 + raise SystemExit( 100 + f"variant {flag_value!r} not supported on {os_name}/{arch}" 101 + ) 102 + return flag_value 103 + 104 + if flag_value == "coreml": 105 + if os_name != "darwin": 106 + raise SystemExit(f"variant 'coreml' not supported on {os_name}") 107 + if arch != "arm64": 108 + raise SystemExit(f"variant 'coreml' not supported on {os_name}/{arch}") 109 + return flag_value 110 + 67 111 if os_name == "darwin" and arch == "arm64": 68 112 return "coreml" 69 113 if os_name == "linux" and arch == "x86_64": 70 - override = os.getenv(PARAKEET_ONNX_VARIANT_ENV) 71 - if override: 72 - if override not in {"cpu", "cuda"}: 114 + if env_value: 115 + if env_value not in {"cpu", "cuda"}: 73 116 raise SystemExit( 74 - f"invalid {PARAKEET_ONNX_VARIANT_ENV}={override!r}; use 'cpu' or 'cuda'" 117 + f"invalid {PARAKEET_ONNX_VARIANT_ENV}={env_value!r}; use 'cpu' or 'cuda'" 75 118 ) 76 - return override 77 - try: 78 - result = subprocess.run( 79 - ["nvidia-smi", "-L"], 80 - check=False, 81 - capture_output=True, 82 - text=True, 83 - ) 84 - except OSError: 85 - return "cpu" 86 - if result.returncode == 0 and result.stdout.strip(): 87 - return "cuda" 88 - return "cpu" 119 + return env_value 120 + return _detect_linux_variant() 89 121 return None 90 122 91 123 ··· 224 256 return "429" in message and "rate" in message and "limit" in message 225 257 226 258 259 + def _verify_bundled_assets() -> None: 260 + for asset_path, expected_sha256 in ( 261 + (WESPEAKER_MODEL_PATH, WESPEAKER_MODEL_SHA256), 262 + (PYANNOTE_OVERLAP_MODEL_PATH, PYANNOTE_OVERLAP_MODEL_SHA256), 263 + ): 264 + try: 265 + actual_sha256 = compute_file_sha256(asset_path) 266 + except OSError as exc: 267 + raise RuntimeError( 268 + f"bundled asset SHA mismatch: {asset_path}\n" 269 + f" expected: {expected_sha256}\n" 270 + f" actual: unavailable ({exc})" 271 + ) from exc 272 + if actual_sha256 != expected_sha256: 273 + raise RuntimeError( 274 + f"bundled asset SHA mismatch: {asset_path}\n" 275 + f" expected: {expected_sha256}\n" 276 + f" actual: {actual_sha256}" 277 + ) 278 + 279 + 227 280 def _verify_linux_cache(cache_dir: Path) -> bool: 228 281 snapshots_dir = cache_dir / "snapshots" 229 282 if not snapshots_dir.is_dir(): ··· 250 303 ) 251 304 252 305 306 + def _verify_variant_cache(variant: str, cache_dir: Path) -> bool: 307 + if variant in {"cpu", "cuda"}: 308 + return _verify_linux_cache(cache_dir) 309 + return _verify_mac_cache(cache_dir) 310 + 311 + 312 + def _check_parakeet_ready( 313 + os_name: str, 314 + arch: str, 315 + variant: str, 316 + sentinel_path: Path, 317 + ) -> Path: 318 + ready_cache = _sentinel_ready( 319 + _load_sentinel(sentinel_path), 320 + os_name, 321 + arch, 322 + variant, 323 + ) 324 + if ready_cache is None: 325 + raise RuntimeError( 326 + f"parakeet check failed: sentinel not ready at {sentinel_path}" 327 + ) 328 + if not _verify_variant_cache(variant, ready_cache): 329 + raise RuntimeError( 330 + f"parakeet check failed: cache verification failed at {ready_cache}" 331 + ) 332 + return ready_cache 333 + 334 + 253 335 def _restore_linux_sentinel_if_cache_ready( 254 336 os_name: str, 255 337 arch: str, ··· 327 409 helper_path = _helper_path() 328 410 if not helper_path.is_file() or not os.access(helper_path, os.X_OK): 329 411 raise RuntimeError( 330 - f"parakeet install failed: helper not found or not executable at {helper_path}" 412 + "parakeet install failed: helper not found or not executable at " 413 + f"{helper_path} run `make parakeet-helper` from the solstone repo to build it" 331 414 ) 332 415 fixture_audio = _fixture_audio_path() 333 416 if not fixture_audio.is_file(): ··· 444 527 445 528 def main() -> int: 446 529 parser = argparse.ArgumentParser( 447 - description="Install or verify the Parakeet model cache." 530 + description=( 531 + "Install and verify solstone's bundled ML models (parakeet ASR plus " 532 + "bundled wespeaker/pyannote assets). Default action checks the " 533 + "parakeet sentinel + cache and fetches if missing; --force re-fetches; " 534 + "--check verifies only and exits nonzero on any problem." 535 + ) 448 536 ) 449 537 mode_group = parser.add_mutually_exclusive_group() 450 538 mode_group.add_argument( 451 539 "--check", 452 540 action="store_true", 453 - help="Verify the sentinel/cache, fetching the model if needed (default behavior).", 541 + help="Verify bundled assets and the parakeet sentinel/cache without fetching.", 454 542 ) 455 543 mode_group.add_argument( 456 544 "--force", 457 545 action="store_true", 458 546 help="Ignore the sentinel and refetch/verify the model cache.", 459 547 ) 548 + parser.add_argument( 549 + "--variant", 550 + choices=("auto", "cpu", "cuda", "coreml"), 551 + default="auto", 552 + help=( 553 + "Parakeet variant to install or verify. auto honors " 554 + "PARAKEET_ONNX_VARIANT on linux/x86_64, then autodetects." 555 + ), 556 + ) 460 557 args = parser.parse_args() 461 558 462 559 os_name, arch = _platform_info() 463 - variant = _variant_for_platform(os_name, arch) 560 + variant = _resolve_variant( 561 + args.variant, 562 + os.getenv(PARAKEET_ONNX_VARIANT_ENV), 563 + os_name, 564 + arch, 565 + ) 566 + 567 + try: 568 + # Why: local asset corruption makes downloading parakeet pointless. 569 + _verify_bundled_assets() 570 + except RuntimeError as exc: 571 + return _fail(str(exc)) 572 + 464 573 if variant is None: 465 - return _fail(f"unsupported parakeet platform: {os_name}/{arch}", code=2) 574 + print( 575 + "parakeet install: unsupported platform " 576 + f"{os_name}/{arch}; supported: darwin/arm64, linux/x86_64" 577 + ) 578 + return 0 579 + 466 580 if os_name == "linux" and arch == "x86_64" and variant == "cuda": 467 581 try: 468 582 import onnxruntime ··· 481 595 482 596 sentinel_path = _sentinel_path(variant) 483 597 cache_dir = _cache_dir(variant) 598 + if args.check: 599 + try: 600 + ready_cache = _check_parakeet_ready( 601 + os_name, 602 + arch, 603 + variant, 604 + sentinel_path, 605 + ) 606 + except RuntimeError as exc: 607 + return _fail(str(exc)) 608 + print(f"model ready: {ready_cache}") 609 + return 0 610 + 484 611 if not args.force: 485 612 ready_cache = _sentinel_ready( 486 613 _load_sentinel(sentinel_path), ··· 488 615 arch, 489 616 variant, 490 617 ) 491 - if ready_cache is not None: 618 + if ready_cache is not None and _verify_variant_cache(variant, ready_cache): 492 619 print(f"model ready: {ready_cache}") 493 620 return 0 494 621 restored_cache = _restore_linux_sentinel_if_cache_ready(
+183
tests/test_install_models.py
··· 1 + # SPDX-License-Identifier: AGPL-3.0-only 2 + # Copyright (c) 2026 sol pbc 3 + 4 + from __future__ import annotations 5 + 6 + import hashlib 7 + import sys 8 + from pathlib import Path 9 + 10 + import pytest 11 + 12 + from think import install_models 13 + 14 + 15 + def _sha256(data: bytes) -> str: 16 + return hashlib.sha256(data).hexdigest() 17 + 18 + 19 + def _write_model_files(base_dir: Path, relative_paths: tuple[str, ...]) -> None: 20 + for relative_path in relative_paths: 21 + target = base_dir / relative_path 22 + target.parent.mkdir(parents=True, exist_ok=True) 23 + target.write_bytes(b"ok") 24 + 25 + 26 + @pytest.mark.parametrize( 27 + ("flag_value", "env_value", "os_name", "arch", "expected"), 28 + [ 29 + ("cpu", None, "linux", "x86_64", "cpu"), 30 + ("cpu", "cuda", "linux", "x86_64", "cpu"), 31 + ("auto", "cpu", "linux", "x86_64", "cpu"), 32 + ("auto", "cuda", "linux", "x86_64", "cuda"), 33 + ("auto", None, "darwin", "arm64", "coreml"), 34 + ("auto", None, "windows", "amd64", None), 35 + ], 36 + ) 37 + def test_resolve_variant_precedence( 38 + monkeypatch: pytest.MonkeyPatch, 39 + flag_value: str, 40 + env_value: str | None, 41 + os_name: str, 42 + arch: str, 43 + expected: str | None, 44 + ): 45 + monkeypatch.setattr(install_models, "_detect_linux_variant", lambda: "cpu") 46 + 47 + assert ( 48 + install_models._resolve_variant(flag_value, env_value, os_name, arch) 49 + == expected 50 + ) 51 + 52 + 53 + def test_resolve_variant_autodetects_linux_gpu(monkeypatch: pytest.MonkeyPatch): 54 + monkeypatch.setattr(install_models, "_detect_linux_variant", lambda: "cuda") 55 + 56 + assert install_models._resolve_variant("auto", None, "linux", "x86_64") == "cuda" 57 + 58 + 59 + def test_resolve_variant_rejects_invalid_env_value(): 60 + with pytest.raises(SystemExit, match="invalid PARAKEET_ONNX_VARIANT='bogus'"): 61 + install_models._resolve_variant("auto", "bogus", "linux", "x86_64") 62 + 63 + 64 + def test_resolve_variant_rejects_incompatible_explicit_variant(): 65 + with pytest.raises(SystemExit, match="variant 'coreml' not supported on linux"): 66 + install_models._resolve_variant("coreml", None, "linux", "x86_64") 67 + with pytest.raises(SystemExit, match="variant 'cpu' not supported on darwin"): 68 + install_models._resolve_variant("cpu", None, "darwin", "arm64") 69 + 70 + 71 + def test_verify_bundled_assets_returns_when_hashes_match( 72 + monkeypatch: pytest.MonkeyPatch, 73 + tmp_path: Path, 74 + ): 75 + wespeaker = tmp_path / "wespeaker.onnx" 76 + pyannote = tmp_path / "pyannote.onnx" 77 + wespeaker.write_bytes(b"wespeaker") 78 + pyannote.write_bytes(b"pyannote") 79 + monkeypatch.setattr(install_models, "WESPEAKER_MODEL_PATH", wespeaker) 80 + monkeypatch.setattr(install_models, "WESPEAKER_MODEL_SHA256", _sha256(b"wespeaker")) 81 + monkeypatch.setattr(install_models, "PYANNOTE_OVERLAP_MODEL_PATH", pyannote) 82 + monkeypatch.setattr( 83 + install_models, 84 + "PYANNOTE_OVERLAP_MODEL_SHA256", 85 + _sha256(b"pyannote"), 86 + ) 87 + 88 + install_models._verify_bundled_assets() 89 + 90 + 91 + def test_verify_bundled_assets_reports_mutated_asset( 92 + monkeypatch: pytest.MonkeyPatch, 93 + tmp_path: Path, 94 + ): 95 + wespeaker = tmp_path / "wespeaker.onnx" 96 + pyannote = tmp_path / "pyannote.onnx" 97 + wespeaker.write_bytes(b"mutated") 98 + pyannote.write_bytes(b"pyannote") 99 + expected = _sha256(b"original") 100 + actual = _sha256(b"mutated") 101 + monkeypatch.setattr(install_models, "WESPEAKER_MODEL_PATH", wespeaker) 102 + monkeypatch.setattr(install_models, "WESPEAKER_MODEL_SHA256", expected) 103 + monkeypatch.setattr(install_models, "PYANNOTE_OVERLAP_MODEL_PATH", pyannote) 104 + monkeypatch.setattr( 105 + install_models, 106 + "PYANNOTE_OVERLAP_MODEL_SHA256", 107 + _sha256(b"pyannote"), 108 + ) 109 + 110 + with pytest.raises(RuntimeError) as exc_info: 111 + install_models._verify_bundled_assets() 112 + 113 + message = str(exc_info.value) 114 + assert f"bundled asset SHA mismatch: {wespeaker}" in message 115 + assert f"expected: {expected}" in message 116 + assert f"actual: {actual}" in message 117 + 118 + 119 + def test_verify_returns_true_when_files_at_fluidaudio_sibling(tmp_path: Path): 120 + cache_dir = tmp_path / "models" 121 + repo_dir = tmp_path / install_models.MAC_FLUIDAUDIO_REPO_NAME 122 + _write_model_files(repo_dir, install_models.MAC_MODEL_FILES) 123 + 124 + assert install_models._verify_mac_cache(cache_dir) is True 125 + 126 + 127 + def test_verify_returns_false_when_sibling_empty(tmp_path: Path): 128 + cache_dir = tmp_path / "models" 129 + cache_dir.mkdir() 130 + (tmp_path / install_models.MAC_FLUIDAUDIO_REPO_NAME).mkdir() 131 + 132 + assert install_models._verify_mac_cache(cache_dir) is False 133 + 134 + 135 + def test_verify_returns_false_when_files_at_literal_path(tmp_path: Path): 136 + cache_dir = tmp_path / "models" 137 + _write_model_files(cache_dir, install_models.MAC_MODEL_FILES) 138 + 139 + assert install_models._verify_mac_cache(cache_dir) is False 140 + 141 + 142 + def _prepare_check_main( 143 + monkeypatch: pytest.MonkeyPatch, 144 + tmp_path: Path, 145 + ) -> tuple[Path, Path]: 146 + sentinel_path = tmp_path / "sentinel.json" 147 + cache_dir = tmp_path / "cache" 148 + monkeypatch.setattr(sys, "argv", ["sol install-models", "--check"]) 149 + monkeypatch.delenv(install_models.PARAKEET_ONNX_VARIANT_ENV, raising=False) 150 + monkeypatch.setattr(install_models, "_platform_info", lambda: ("linux", "x86_64")) 151 + monkeypatch.setattr(install_models, "_detect_linux_variant", lambda: "cpu") 152 + monkeypatch.setattr(install_models, "_verify_bundled_assets", lambda: None) 153 + monkeypatch.setattr(install_models, "_sentinel_path", lambda variant: sentinel_path) 154 + monkeypatch.setattr(install_models, "_cache_dir", lambda variant: cache_dir) 155 + return sentinel_path, cache_dir 156 + 157 + 158 + def test_main_check_missing_sentinel_returns_nonzero( 159 + monkeypatch: pytest.MonkeyPatch, 160 + tmp_path: Path, 161 + capsys: pytest.CaptureFixture[str], 162 + ): 163 + _prepare_check_main(monkeypatch, tmp_path) 164 + 165 + assert install_models.main() == 1 166 + assert "parakeet check failed: sentinel not ready" in capsys.readouterr().err 167 + 168 + 169 + def test_main_check_ready_cache_returns_zero( 170 + monkeypatch: pytest.MonkeyPatch, 171 + tmp_path: Path, 172 + capsys: pytest.CaptureFixture[str], 173 + ): 174 + sentinel_path, cache_dir = _prepare_check_main(monkeypatch, tmp_path) 175 + cache_dir.mkdir() 176 + install_models._write_sentinel( 177 + sentinel_path, 178 + install_models._build_payload("linux", "x86_64", "cpu", cache_dir), 179 + ) 180 + monkeypatch.setattr(install_models, "_verify_linux_cache", lambda path: True) 181 + 182 + assert install_models.main() == 0 183 + assert f"model ready: {cache_dir}" in capsys.readouterr().out
-93
tests/test_install_parakeet_model.py
··· 1 - # SPDX-License-Identifier: AGPL-3.0-only 2 - # Copyright (c) 2026 sol pbc 3 - 4 - from __future__ import annotations 5 - 6 - import json 7 - import subprocess 8 - import sys 9 - import tempfile 10 - from pathlib import Path 11 - 12 - ROOT = Path(__file__).resolve().parent.parent 13 - SCRIPT = ROOT / "scripts" / "install_parakeet_model.py" 14 - _PROBE = """ 15 - import json 16 - import runpy 17 - import sys 18 - from pathlib import Path 19 - 20 - ns = runpy.run_path(sys.argv[1]) 21 - action = sys.argv[2] 22 - 23 - if action == "constants": 24 - print( 25 - json.dumps( 26 - { 27 - "repo_name": ns["MAC_FLUIDAUDIO_REPO_NAME"], 28 - "model_files": ns["MAC_MODEL_FILES"], 29 - } 30 - ) 31 - ) 32 - elif action == "verify": 33 - print(json.dumps({"ok": ns["_verify_mac_cache"](Path(sys.argv[3]))})) 34 - else: 35 - raise AssertionError(f"unknown action: {action}") 36 - """ 37 - 38 - 39 - def _probe(action: str, cache_dir: Path | None = None) -> dict[str, object]: 40 - argv = [sys.executable, "-c", _PROBE, str(SCRIPT), action] 41 - if cache_dir is not None: 42 - argv.append(str(cache_dir)) 43 - result = subprocess.run(argv, check=True, capture_output=True, text=True) 44 - return json.loads(result.stdout) 45 - 46 - 47 - def _load_constants() -> tuple[str, tuple[str, ...]]: 48 - data = _probe("constants") 49 - return data["repo_name"], tuple(data["model_files"]) 50 - 51 - 52 - def _verify_mac_cache(cache_dir: Path) -> bool: 53 - data = _probe("verify", cache_dir) 54 - return bool(data["ok"]) 55 - 56 - 57 - def _write_model_files(base_dir: Path, relative_paths: tuple[str, ...]) -> None: 58 - for relative_path in relative_paths: 59 - target = base_dir / relative_path 60 - target.parent.mkdir(parents=True, exist_ok=True) 61 - target.write_bytes(b"ok") 62 - 63 - 64 - def test_verify_returns_true_when_files_at_fluidaudio_sibling(): 65 - with tempfile.TemporaryDirectory() as tmp_dir: 66 - repo_name, model_files = _load_constants() 67 - tmp_path = Path(tmp_dir) 68 - cache_dir = tmp_path / "models" 69 - repo_dir = tmp_path / repo_name 70 - _write_model_files(repo_dir, model_files) 71 - 72 - assert _verify_mac_cache(cache_dir) is True 73 - 74 - 75 - def test_verify_returns_false_when_sibling_empty(): 76 - with tempfile.TemporaryDirectory() as tmp_dir: 77 - repo_name, _ = _load_constants() 78 - tmp_path = Path(tmp_dir) 79 - cache_dir = tmp_path / "models" 80 - cache_dir.mkdir() 81 - (tmp_path / repo_name).mkdir() 82 - 83 - assert _verify_mac_cache(cache_dir) is False 84 - 85 - 86 - def test_verify_returns_false_when_files_at_literal_path(): 87 - with tempfile.TemporaryDirectory() as tmp_dir: 88 - _, model_files = _load_constants() 89 - tmp_path = Path(tmp_dir) 90 - cache_dir = tmp_path / "models" 91 - _write_model_files(cache_dir, model_files) 92 - 93 - assert _verify_mac_cache(cache_dir) is False
+2
think/sol_cli.py
··· 48 48 "notify": "think.notify_cli", 49 49 "doctor": "think.doctor", 50 50 "config": "think.config_cli", 51 + "install-models": "think.install_models", 51 52 "password": "think.password_cli", 52 53 "streams": "think.streams", 53 54 "segment": "think.segment", ··· 126 127 "restart-convey", 127 128 "maint", 128 129 ], 130 + "Setup": ["install-models"], 129 131 "Specialized tools": [ 130 132 "password", 131 133 "config",