Files
VoiceStudio/tests/test_mlx_gate_390.py
Palash DebnathandClaude Opus 4.8 c3b2346759 fix(engines): MLX platform gate (#390) + ASR gpu_compat + IndexTTS2 (#21 PR 2/5) (#431)
Builds on the device probe from PR 1. Backend-only; the routing keys are
wired into /engines in PR 3.

- #390 closed: MLXAudioBackend / MLXWhisperBackend now call the shared
  `core.device_caps.mlx_supported()` gate FIRST, before importing the
  package. On Linux/Windows/mac-Intel they report unavailable and never
  advertise a usable `mps` route, even with a stray mlx wheel installed.
  Replaces the ASR backend's ad-hoc inline MPS check with the one shared
  rule. (The Wave-4.4 OSError/RuntimeError import-guard is preserved — it
  now lives behind the platform gate; its test forces the gate open so the
  guard stays the path under test.)
- `ASRBackend` ABC gains `gpu_compat: tuple[str, ...] = ("cpu",)` mirroring
  TTSBackend, and each subclass declares its real targets:
  whisperx/faster-whisper → (cuda,cpu); mlx-whisper → (mps,cpu);
  pytorch-whisper → (cuda,mps,cpu); nemo/funasr → (cuda,cpu);
  moonshine → (cpu,). Inert until PR 3 serializes them.
- IndexTTS2 declares `gpu_compat = ("cuda","cpu")` so it stops advertising
  the inherited CPU-only default.
- ROCm is deliberately NOT claimed for any ASR engine (or for IndexTTS2):
  CTranslate2 has no upstream HIP build, and an unverified `rocm` claim
  would route ROCm hosts to a broken GPU path — strictly worse than the
  honest `cpu_fallback` the resolver already emits ("declares CUDA only;
  ROCm not in its compat set"). The per-engine TTS ROCm audit is a tracked
  follow-up that will verify each path before claiming it.

Tests: MLX gate regression (both backends, on/off Apple), ASR gpu_compat
tuples + no-false-rocm invariant, IndexTTS2 override; existing MLX
import-guard test updated for the new gate ordering.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 01:43:21 +05:30

51 lines
2.0 KiB
Python

"""#390 regression: MLX-Audio and MLX-Whisper must report unavailable (and never
advertise a usable `mps` route) on non-Apple-Silicon hosts, even with the
package importable. Both backends now route through the shared
``core.device_caps.mlx_supported()`` gate BEFORE importing the package.
Backend classes are resolved at RUNTIME (other suites purge ``services.*`` from
``sys.modules``; importing the classes at module scope risks stale references
depending on test order).
"""
from __future__ import annotations
import pytest
_MLX_BACKENDS = [
("services.tts_backend", "MLXAudioBackend"),
("services.asr_backend", "MLXWhisperBackend"),
]
def _resolve(module_path, cls_name):
import importlib
return getattr(importlib.import_module(module_path), cls_name)
@pytest.mark.parametrize("module_path,cls_name", _MLX_BACKENDS)
def test_gate_blocks_when_mlx_unsupported(module_path, cls_name, monkeypatch):
monkeypatch.setattr(
"core.device_caps.mlx_supported",
lambda: (False, "MLX requires Apple Silicon; this host is linux/x86_64"),
)
ok, why = _resolve(module_path, cls_name).is_available()
assert ok is False
assert "Apple Silicon" in why
@pytest.mark.parametrize("module_path,cls_name", _MLX_BACKENDS)
def test_gate_passthrough_when_supported(module_path, cls_name, monkeypatch):
# When the platform gate passes, is_available proceeds to the package import.
monkeypatch.setattr("core.device_caps.mlx_supported", lambda: (True, ""))
ok, why = _resolve(module_path, cls_name).is_available()
# Proof the gate didn't short-circuit: control reached the package-import
# branch — so it's either genuinely available, or it reports the *package*
# error ("… unavailable"), never the platform-gate string.
assert ok or "unavailable" in why
assert not why.startswith("MLX requires Apple Silicon")
@pytest.mark.parametrize("module_path,cls_name", _MLX_BACKENDS)
def test_mlx_backends_declare_mps_cpu(module_path, cls_name):
assert _resolve(module_path, cls_name).gpu_compat == ("mps", "cpu")