Files
VoiceStudio/tests/test_asr_gpu_compat.py
T
debpalashandClaude Fable 5 63fd497caf feat: TTS-only first run, platform-curated ASR, guided OS permissions, parakeet-mlx
Only the TTS model (~2.4 GB) is required on first run; ASR models are
per-platform curated picks (curated_on in models.yaml) installed on demand.
Every transcription surface returns a typed asr_model_missing error with a
one-click download CTA instead of silently pulling multi-GB Whisper weights.
Settings -> Models is a grouped, platform-aware catalog. New guided
permissions UX (wizard System Check + Settings -> Permissions + mic
pre-flight) with native mic-state checks and OS settings deep-links. New
parakeet-mlx engine brings Parakeet TDT v3 to Apple Silicon (language-gated
capture preference so multilingual dictation never regresses). Docs:
expressive-speech page, Flush/Unload + CPU-fallback triage, clone-length FAQ.
Hardening: preflight fails open for custom model pins, ROCm curation no
longer inherits NVIDIA picks, Windows mic probe reads the NonPackaged
consent key, CaptureWidget setup race fixed, offline-cache CI simulation
fixes so empty-cache runners stay green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 15:20:54 +05:30

90 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""ASR backends now carry an explicit ``gpu_compat`` (mirroring TTSBackend) so
engine_routing can surface the effective device per host. Verifies the ABC
default and every subclass's declared tuple, plus the IndexTTS2 fix.
Backend classes are resolved at RUNTIME (inside each test, via the registry)
rather than imported at module scope — other suites purge ``services.*`` from
``sys.modules`` for DB isolation, which would otherwise leave this module
holding stale class objects depending on collection/run order.
"""
from __future__ import annotations
import pytest
# id → declared gpu_compat. nemo-parakeet was CUDA-gated until 2026-07-02,
# when parakeet-tdt-0.6b-v3 was measured at RTF 0.08–0.23 on an M2 CPU —
# every ASR engine now has a cpu path.
_EXPECTED = {
"whisperx": ("cuda", "cpu"),
"faster-whisper": ("cuda", "cpu"),
"mlx-whisper": ("mps", "cpu"),
"pytorch-whisper": ("cuda", "mps", "cpu"),
"nemo-parakeet": ("cuda", "cpu"),
# MLX runs on Apple Silicon's unified-memory GPU only; is_available
# hard-gates on mlx_supported(), so claiming cpu would be false.
"parakeet-mlx": ("mps",),
"moonshine": ("cpu",),
"funasr": ("cuda", "cpu"),
# Crash-isolated sidecar wraps the same CTranslate2 engine as
# faster-whisper — same device support (#730 residual B).
"faster-whisper-isolated": ("cuda", "cpu"),
}
# Engines that legitimately have NO cpu path (hard platform/GPU gate in
# is_available — e.g. parakeet-mlx gates on Apple Silicon via mlx_supported()).
_GPU_ONLY: set[str] = {"parakeet-mlx"}
_VALID = {"cuda", "rocm", "mps", "xpu", "cpu"}
def _cls(engine_id):
from services.asr_backend import _REGISTRY
return _REGISTRY[engine_id]
def test_abc_default_is_cpu_only():
from services.asr_backend import ASRBackend
assert ASRBackend.gpu_compat == ("cpu",)
@pytest.mark.parametrize("engine_id,expected", list(_EXPECTED.items()))
def test_subclass_gpu_compat(engine_id, expected):
assert _cls(engine_id).gpu_compat == expected
@pytest.mark.parametrize("engine_id", list(_EXPECTED))
def test_compat_values_are_valid(engine_id):
compat = _cls(engine_id).gpu_compat
assert compat, "gpu_compat must be non-empty"
assert set(compat) <= _VALID
# Every engine has a cpu path EXCEPT the known hard-GPU-gated ones.
if engine_id not in _GPU_ONLY:
assert "cpu" in compat
else:
assert "cpu" not in compat # would be a false claim — is_available gates on CUDA
def test_no_asr_engine_falsely_claims_rocm():
# ROCm is intentionally unclaimed until verified per engine (see ABC note).
for engine_id in _EXPECTED:
assert "rocm" not in _cls(engine_id).gpu_compat
def test_nemo_parakeet_has_no_cuda_gate(monkeypatch):
"""Regression (CPU un-gating, 2026-07-02): on a CUDA-less host,
is_available() must never claim a GPU is required — availability is a
pure nemo_toolkit dependency check now."""
import torch
monkeypatch.setattr(torch.cuda, "is_available", lambda: False)
ok, reason = _cls("nemo-parakeet").is_available()
assert "NVIDIA GPU" not in reason
if not ok: # env without nemo_toolkit — the only legitimate blocker
assert "nemo_toolkit" in reason
def test_indextts2_overrides_cpu_only_default():
from engines.indextts import IndexTTS2Backend
assert IndexTTS2Backend.gpu_compat == ("cuda", "cpu")
# must NOT be the inherited TTSBackend default
assert IndexTTS2Backend.gpu_compat != ("cpu",)