get_recommended_model() now keys to the backend the installed whisper.cpp build actually has (detect_pywhispercpp_gpu_backend) and to free memory measured at recommendation time: free VRAM on a discrete GPU for a Vulkan or CUDA build, free system RAM otherwise. The result is a rung of the model ladder, so a Vulkan-capable Nvidia card reaches the ladder instead of the total-memory tiers, and unquantized turbo, the large family and medium are never a default. Manual selections are untouched. The informational hardware probe no longer decides the model. Dead VRAM-total parsing is removed. free_memory_for_backend() holds the source choice; the floor-rung map is shared by rung selection and step-down.
1204 lines
48 KiB
Python
1204 lines
48 KiB
Python
"""
|
|
Comprehensive coverage tests for whispercpp_model_info module.
|
|
|
|
Tests all functions and constants in the module.
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from typing import Any, Optional
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from vocalinux.utils import whispercpp_model_info as _whispercpp_model_info
|
|
|
|
_REAL_GGML_DEVICE_PROBE = _whispercpp_model_info.detect_ggml_gpu_devices
|
|
|
|
|
|
def _fake_ggml_library(registry: list[tuple[str, str, int]]) -> MagicMock:
|
|
library = MagicMock()
|
|
library.ggml_backend_dev_count.return_value = len(registry)
|
|
|
|
def _get_device(index: int) -> int:
|
|
return index + 1
|
|
|
|
def _get_device_type(pointer: int) -> int:
|
|
return registry[pointer - 1][2]
|
|
|
|
def _get_device_name(pointer: int) -> bytes:
|
|
return registry[pointer - 1][0].encode("utf-8")
|
|
|
|
def _get_device_description(pointer: int) -> bytes:
|
|
return registry[pointer - 1][1].encode("utf-8")
|
|
|
|
library.ggml_backend_dev_get.side_effect = _get_device
|
|
library.ggml_backend_dev_type.side_effect = _get_device_type
|
|
library.ggml_backend_dev_name.side_effect = _get_device_name
|
|
library.ggml_backend_dev_description.side_effect = _get_device_description
|
|
return library
|
|
|
|
|
|
def _run_ggml_probe(
|
|
registry: list[tuple[str, str, int]],
|
|
version_string: str = "1.5.0",
|
|
library: Optional[Any] = None,
|
|
library_error: Optional[Exception] = None,
|
|
missing_extension: bool = False,
|
|
) -> Optional[list[dict[str, Any]]]:
|
|
_REAL_GGML_DEVICE_PROBE.cache_clear()
|
|
native_library = library if library is not None else _fake_ggml_library(registry)
|
|
extension = None if missing_extension else SimpleNamespace(__file__="/fake/_pywhispercpp.so")
|
|
from vocalinux.utils import pywhispercpp_loader
|
|
|
|
with (
|
|
patch.object(_whispercpp_model_info, "version", return_value=version_string),
|
|
patch.object(pywhispercpp_loader, "preload_shared_libraries"),
|
|
patch.dict(sys.modules, {"_pywhispercpp": extension}),
|
|
):
|
|
if library_error is not None:
|
|
with patch.object(_whispercpp_model_info.ctypes, "CDLL", side_effect=library_error):
|
|
return _REAL_GGML_DEVICE_PROBE()
|
|
with patch.object(_whispercpp_model_info.ctypes, "CDLL", return_value=native_library):
|
|
return _REAL_GGML_DEVICE_PROBE()
|
|
|
|
|
|
class TestComputeBackendClass(unittest.TestCase):
|
|
"""Test cases for ComputeBackend class."""
|
|
|
|
def test_compute_backend_has_vulkan_constant(self):
|
|
"""Test that ComputeBackend has VULKAN constant."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend
|
|
|
|
self.assertTrue(hasattr(ComputeBackend, "VULKAN"))
|
|
self.assertEqual(ComputeBackend.VULKAN, "vulkan")
|
|
|
|
def test_compute_backend_has_cuda_constant(self):
|
|
"""Test that ComputeBackend has CUDA constant."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend
|
|
|
|
self.assertTrue(hasattr(ComputeBackend, "CUDA"))
|
|
self.assertEqual(ComputeBackend.CUDA, "cuda")
|
|
|
|
def test_compute_backend_has_cpu_constant(self):
|
|
"""Test that ComputeBackend has CPU constant."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend
|
|
|
|
self.assertTrue(hasattr(ComputeBackend, "CPU"))
|
|
self.assertEqual(ComputeBackend.CPU, "cpu")
|
|
|
|
|
|
class TestModelInfo(unittest.TestCase):
|
|
"""Test cases for model information constants."""
|
|
|
|
def test_whispercpp_model_info_exists(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO is defined."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIsInstance(WHISPERCPP_MODEL_INFO, dict)
|
|
self.assertGreater(len(WHISPERCPP_MODEL_INFO), 0)
|
|
|
|
def test_whispercpp_model_info_contains_tiny(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains tiny model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("tiny", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_whispercpp_model_info_contains_base(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains base model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("base", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_whispercpp_model_info_contains_small(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains small model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("small", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_whispercpp_model_info_contains_medium(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains medium model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("medium", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_whispercpp_model_info_contains_large(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains large model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("large", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_whispercpp_model_info_contains_large_v3_turbo(self):
|
|
"""Test that WHISPERCPP_MODEL_INFO contains large-v3-turbo model."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("large-v3-turbo", WHISPERCPP_MODEL_INFO)
|
|
|
|
def test_model_info_has_size_mb(self):
|
|
"""Test that each model has size_mb field."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
|
|
self.assertIn("size_mb", model_info, f"{model_name} missing size_mb")
|
|
self.assertIsInstance(model_info["size_mb"], int)
|
|
self.assertGreater(model_info["size_mb"], 0)
|
|
|
|
def test_model_info_has_params(self):
|
|
"""Test that each model has params field."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
|
|
self.assertIn("params", model_info, f"{model_name} missing params")
|
|
self.assertIsInstance(model_info["params"], str)
|
|
|
|
def test_model_info_has_desc(self):
|
|
"""Test that each model has desc field."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
|
|
self.assertIn("desc", model_info, f"{model_name} missing desc")
|
|
self.assertIsInstance(model_info["desc"], str)
|
|
|
|
def test_model_info_has_url(self):
|
|
"""Test that each model has url field."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
for model_name, model_info in WHISPERCPP_MODEL_INFO.items():
|
|
self.assertIn("url", model_info, f"{model_name} missing url")
|
|
self.assertIsInstance(model_info["url"], str)
|
|
self.assertTrue(model_info["url"].startswith("https://"))
|
|
|
|
def test_available_models_list_exists(self):
|
|
"""Test that AVAILABLE_MODELS list exists."""
|
|
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS
|
|
|
|
self.assertIsInstance(AVAILABLE_MODELS, list)
|
|
self.assertGreater(len(AVAILABLE_MODELS), 0)
|
|
|
|
def test_available_models_contains_all_models(self):
|
|
"""Test that AVAILABLE_MODELS contains all defined models."""
|
|
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS, WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertEqual(set(AVAILABLE_MODELS), set(WHISPERCPP_MODEL_INFO.keys()))
|
|
|
|
def test_available_models_tracks_upstream_dictation_models(self):
|
|
"""Test that AVAILABLE_MODELS covers upstream dictation models."""
|
|
from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS
|
|
|
|
expected_models = [
|
|
"tiny",
|
|
"tiny.en",
|
|
"tiny-q5_1",
|
|
"tiny.en-q5_1",
|
|
"tiny-q8_0",
|
|
"base",
|
|
"base.en",
|
|
"base-q5_1",
|
|
"base.en-q5_1",
|
|
"base-q8_0",
|
|
"small",
|
|
"small.en",
|
|
"small-q5_1",
|
|
"small.en-q5_1",
|
|
"small-q8_0",
|
|
"medium",
|
|
"medium.en",
|
|
"medium-q5_0",
|
|
"medium.en-q5_0",
|
|
"medium-q8_0",
|
|
"large-v1",
|
|
"large-v2",
|
|
"large-v2-q5_0",
|
|
"large-v2-q8_0",
|
|
"large", # Voculinux alias for upstream large-v3
|
|
"large-v3-q5_0",
|
|
"large-v3-turbo",
|
|
"large-v3-turbo-q5_0",
|
|
"large-v3-turbo-q8_0",
|
|
"small.en-tdrz",
|
|
]
|
|
|
|
self.assertEqual(AVAILABLE_MODELS, expected_models)
|
|
|
|
def test_model_sizes_group_whispercpp_variants_for_ui(self):
|
|
"""Test that whisper.cpp models are grouped into size buckets."""
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
MODEL_SIZES,
|
|
MODEL_VARIANTS_BY_SIZE,
|
|
NON_DICTATION_MODELS,
|
|
WHISPERCPP_MODEL_INFO,
|
|
)
|
|
|
|
self.assertEqual(MODEL_SIZES, ["tiny", "base", "small", "medium", "large"])
|
|
self.assertEqual(MODEL_VARIANTS_BY_SIZE["medium"][0], "medium")
|
|
self.assertIn("medium.en-q5_0", MODEL_VARIANTS_BY_SIZE["medium"])
|
|
self.assertIn("large-v3-turbo-q8_0", MODEL_VARIANTS_BY_SIZE["large"])
|
|
self.assertNotIn("small.en-tdrz", MODEL_VARIANTS_BY_SIZE["small"])
|
|
|
|
grouped_models = {
|
|
model_name for variants in MODEL_VARIANTS_BY_SIZE.values() for model_name in variants
|
|
}
|
|
self.assertEqual(grouped_models, set(WHISPERCPP_MODEL_INFO.keys()) - NON_DICTATION_MODELS)
|
|
self.assertFalse(
|
|
any(model_name.endswith("-tdrz") for model_name in grouped_models),
|
|
"TinyDiarize needs diarization-specific runtime support, not the base dictation picker",
|
|
)
|
|
|
|
def test_tdrz_model_is_catalogued_but_not_a_dictation_model(self):
|
|
"""TinyDiarize is fetchable through the catalog, never dictation-selectable."""
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
NON_DICTATION_MODELS,
|
|
TDRZ_MODEL,
|
|
WHISPERCPP_MODEL_INFO,
|
|
is_dictation_model,
|
|
whispercpp_model_file,
|
|
whispercpp_model_source,
|
|
)
|
|
|
|
self.assertEqual(TDRZ_MODEL, "small.en-tdrz")
|
|
self.assertIn(TDRZ_MODEL, WHISPERCPP_MODEL_INFO)
|
|
self.assertIn(TDRZ_MODEL, NON_DICTATION_MODELS)
|
|
self.assertFalse(is_dictation_model(TDRZ_MODEL))
|
|
self.assertTrue(is_dictation_model("small.en"))
|
|
self.assertFalse(is_dictation_model("not-a-model"))
|
|
|
|
self.assertEqual(whispercpp_model_file(TDRZ_MODEL), "ggml-small.en-tdrz.bin")
|
|
url = WHISPERCPP_MODEL_INFO[TDRZ_MODEL]["url"]
|
|
self.assertIn("akashmjn/tinydiarize-whisper.cpp", url)
|
|
repo, revision = whispercpp_model_source(TDRZ_MODEL)
|
|
self.assertEqual(repo, "akashmjn/tinydiarize-whisper.cpp")
|
|
self.assertTrue(revision)
|
|
self.assertIn(f"/resolve/{revision}/", url)
|
|
|
|
main_repo, main_revision = whispercpp_model_source("small.en")
|
|
self.assertEqual(main_repo, "ggerganov/whisper.cpp")
|
|
self.assertEqual(main_revision, "")
|
|
|
|
def test_model_variant_helpers_identify_size_and_english_only_variants(self):
|
|
"""Test helpers that drive the split whisper.cpp model selectors."""
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
get_model_size,
|
|
get_model_variants,
|
|
is_english_only_model,
|
|
)
|
|
|
|
self.assertEqual(get_model_size("medium.en-q5_0"), "medium")
|
|
self.assertEqual(get_model_size("large-v3-turbo-q8_0"), "large")
|
|
self.assertTrue(is_english_only_model("tiny.en-q5_1"))
|
|
self.assertFalse(is_english_only_model("large-v3-turbo"))
|
|
|
|
|
|
class TestDetectVulkanDevices(unittest.TestCase):
|
|
"""Test cases for detect_vulkan_devices function."""
|
|
|
|
def setUp(self):
|
|
"""Clear lru_cache before each test."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
detect_vulkan_devices.cache_clear()
|
|
|
|
def test_detect_vulkan_devices_empty_when_unavailable(self):
|
|
"""Test that detect_vulkan_devices returns empty list when vulkaninfo unavailable."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(devices, [])
|
|
|
|
def test_detect_vulkan_devices_single_discrete(self):
|
|
"""Test detection of a single discrete GPU from real vulkaninfo --summary."""
|
|
vulkaninfo_output = (
|
|
"Devices:\n"
|
|
"========\n"
|
|
"GPU0:\n"
|
|
"\tapiVersion = 1.3.296\n"
|
|
"\tdriverVersion = 550.54.14\n"
|
|
"\tvendorID = 0x10de\n"
|
|
"\tdeviceID = 0x2206\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA RTX 3080\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(len(devices), 1)
|
|
self.assertEqual(devices[0]["index"], 0)
|
|
self.assertEqual(devices[0]["name"], "NVIDIA RTX 3080")
|
|
self.assertEqual(devices[0]["device_type"], "discrete")
|
|
|
|
def test_detect_vulkan_devices_hybrid_system(self):
|
|
"""Test detection on hybrid system with real vulkaninfo --summary output."""
|
|
vulkaninfo_output = (
|
|
"Devices:\n"
|
|
"========\n"
|
|
"GPU0:\n"
|
|
"\tapiVersion = 1.3.296\n"
|
|
"\tdriverVersion = 24.2.8\n"
|
|
"\tvendorID = 0x8086\n"
|
|
"\tdeviceID = 0x3e92\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
|
|
"GPU1:\n"
|
|
"\tapiVersion = 1.3.242\n"
|
|
"\tdriverVersion = 550.54.14\n"
|
|
"\tvendorID = 0x10de\n"
|
|
"\tdeviceID = 0x1f08\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(len(devices), 2)
|
|
self.assertEqual(devices[0]["index"], 0)
|
|
self.assertEqual(devices[0]["name"], "Intel(R) UHD Graphics 630 (CFL GT2)")
|
|
self.assertEqual(devices[0]["device_type"], "integrated")
|
|
self.assertEqual(devices[1]["index"], 1)
|
|
self.assertEqual(devices[1]["name"], "NVIDIA GeForce RTX 2060")
|
|
self.assertEqual(devices[1]["device_type"], "discrete")
|
|
|
|
def test_detect_vulkan_devices_legacy_gpu_id_format(self):
|
|
"""Test compatibility with alternate 'GPU id = N' vulkaninfo headers."""
|
|
vulkaninfo_output = (
|
|
"GPU id = 0\n"
|
|
"deviceName = Intel UHD 630\n"
|
|
"deviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"GPU id = 1\n"
|
|
"deviceName = NVIDIA RTX 2060\n"
|
|
"deviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(len(devices), 2)
|
|
self.assertEqual(devices[0]["index"], 0)
|
|
self.assertEqual(devices[1]["index"], 1)
|
|
self.assertEqual(devices[1]["device_type"], "discrete")
|
|
|
|
def test_detect_vulkan_devices_on_timeout(self):
|
|
"""Test that timeout returns empty list."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = subprocess.TimeoutExpired("vulkaninfo", 5)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(devices, [])
|
|
|
|
def test_detect_vulkan_devices_on_error(self):
|
|
"""Test that error returncode returns empty list."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(devices, [])
|
|
|
|
def test_detect_vulkan_devices_falls_back_without_summary(self):
|
|
"""Older vulkaninfo without --summary still enumerates GPUs."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA RTX 2060\n"
|
|
)
|
|
|
|
def run_side_effect(cmd, **kwargs):
|
|
if cmd == ["vulkaninfo", "--summary"]:
|
|
return MagicMock(returncode=1, stdout="")
|
|
if cmd == ["vulkaninfo"]:
|
|
return MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
return MagicMock(returncode=1, stdout="")
|
|
|
|
with patch("subprocess.run", side_effect=run_side_effect):
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(len(devices), 1)
|
|
self.assertEqual(devices[0]["name"], "NVIDIA RTX 2060")
|
|
self.assertEqual(devices[0]["device_type"], "discrete")
|
|
|
|
def test_detect_vulkan_devices_marks_llvmpipe_software(self):
|
|
"""Software renderers are classified separately from hardware GPUs."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
|
|
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
|
|
"GPU1:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
devices = detect_vulkan_devices()
|
|
self.assertEqual(devices[0]["device_type"], "software")
|
|
self.assertEqual(devices[1]["device_type"], "discrete")
|
|
|
|
|
|
class TestPreferDiscreteVulkanDevice(unittest.TestCase):
|
|
"""Test cases for _prefer_discrete_vulkan_device function."""
|
|
|
|
def setUp(self):
|
|
"""Clear lru_cache before each test."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
detect_vulkan_devices.cache_clear()
|
|
|
|
def test_prefer_discrete_when_available(self):
|
|
"""Test that discrete GPU is preferred when available."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel UHD 630\n"
|
|
"GPU1:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
|
|
|
|
preferred_idx = _prefer_discrete_vulkan_device()
|
|
self.assertEqual(preferred_idx, 1)
|
|
|
|
def test_prefer_discrete_falls_back_to_first(self):
|
|
"""Test that first device is used when no discrete GPU available."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel UHD 630\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
|
|
|
|
preferred_idx = _prefer_discrete_vulkan_device()
|
|
self.assertEqual(preferred_idx, 0)
|
|
|
|
|
|
class TestDetectGgmlGpuDevices(unittest.TestCase):
|
|
"""Test ggml's native GPU registry probe."""
|
|
|
|
def test_probe_assigns_zero_to_first_discrete_gpu(
|
|
self: "TestDetectGgmlGpuDevices",
|
|
) -> None:
|
|
devices = _run_ggml_probe(
|
|
[
|
|
("Vulkan0", "NVIDIA GeForce GTX 1050", 1),
|
|
("CPU", "CPU backend", 0),
|
|
]
|
|
)
|
|
|
|
self.assertEqual(
|
|
devices,
|
|
[
|
|
{
|
|
"index": 0,
|
|
"name": "NVIDIA GeForce GTX 1050",
|
|
"backend_name": "Vulkan0",
|
|
"device_type": "discrete",
|
|
}
|
|
],
|
|
)
|
|
|
|
def test_probe_uses_gpu_and_igpu_ordinals(self: "TestDetectGgmlGpuDevices") -> None:
|
|
devices = _run_ggml_probe(
|
|
[
|
|
("Vulkan0", "Intel Arc", 2),
|
|
("CPU", "CPU backend", 0),
|
|
("Vulkan1", "NVIDIA GeForce GTX 1050", 1),
|
|
("ACCEL", "Accelerator", 3),
|
|
]
|
|
)
|
|
|
|
self.assertEqual(
|
|
devices,
|
|
[
|
|
{
|
|
"index": 0,
|
|
"name": "Intel Arc",
|
|
"backend_name": "Vulkan0",
|
|
"device_type": "integrated",
|
|
},
|
|
{
|
|
"index": 1,
|
|
"name": "NVIDIA GeForce GTX 1050",
|
|
"backend_name": "Vulkan1",
|
|
"device_type": "discrete",
|
|
},
|
|
],
|
|
)
|
|
|
|
def test_probe_returns_empty_list_for_cpu_only_registry(
|
|
self: "TestDetectGgmlGpuDevices",
|
|
) -> None:
|
|
self.assertEqual(_run_ggml_probe([("CPU", "CPU backend", 0)]), [])
|
|
|
|
def test_probe_returns_none_when_runtime_is_unavailable(
|
|
self: "TestDetectGgmlGpuDevices",
|
|
) -> None:
|
|
self.assertIsNone(_run_ggml_probe([], version_string="1.4.9"))
|
|
self.assertIsNone(_run_ggml_probe([], missing_extension=True))
|
|
self.assertIsNone(_run_ggml_probe([], library_error=OSError("unavailable")))
|
|
self.assertIsNone(_run_ggml_probe([], library=SimpleNamespace()))
|
|
|
|
def test_probe_keeps_ordinal_for_device_without_description(
|
|
self: "TestDetectGgmlGpuDevices",
|
|
) -> None:
|
|
library = _fake_ggml_library(
|
|
[("Vulkan0", "", 1), ("Vulkan1", "NVIDIA GeForce GTX 1050", 1)]
|
|
)
|
|
library.ggml_backend_dev_description.side_effect = lambda pointer: (
|
|
None if pointer == 1 else b"NVIDIA GeForce GTX 1050"
|
|
)
|
|
|
|
devices = _run_ggml_probe([], library=library)
|
|
|
|
assert devices is not None
|
|
self.assertEqual([device["index"] for device in devices], [0, 1])
|
|
self.assertEqual(devices[0]["name"], "Vulkan0")
|
|
self.assertEqual(devices[1]["name"], "NVIDIA GeForce GTX 1050")
|
|
|
|
def test_probe_accepts_prerelease_version(self: "TestDetectGgmlGpuDevices") -> None:
|
|
devices = _run_ggml_probe([("Vulkan0", "GPU", 1)], version_string="1.5.1.dev3")
|
|
assert devices is not None
|
|
self.assertEqual(len(devices), 1)
|
|
|
|
|
|
class TestPreferDiscreteVulkanDeviceMore(unittest.TestCase):
|
|
"""Exercise additional Vulkan selection edge cases."""
|
|
|
|
def setUp(self: "TestPreferDiscreteVulkanDeviceMore") -> None:
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_devices
|
|
|
|
detect_vulkan_devices.cache_clear()
|
|
|
|
def test_prefer_discrete_returns_none_when_empty(self):
|
|
"""Test that None is returned when no devices found."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
|
|
|
|
preferred_idx = _prefer_discrete_vulkan_device()
|
|
self.assertIsNone(preferred_idx)
|
|
|
|
def test_prefer_discrete_skips_software_renderer(self):
|
|
"""Auto-select must not pick llvmpipe when a real GPU exists."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
|
|
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
|
|
"GPU1:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import _prefer_discrete_vulkan_device
|
|
|
|
preferred_idx = _prefer_discrete_vulkan_device()
|
|
self.assertEqual(preferred_idx, 1)
|
|
|
|
def test_prefer_discrete_ignores_software_only(self):
|
|
"""Software-only Vulkan should not produce an auto-selected device."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_CPU\n"
|
|
"\tdeviceName = llvmpipe (LLVM 19.1.7, 256 bits)\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
ComputeBackend,
|
|
_prefer_discrete_vulkan_device,
|
|
detect_compute_backend,
|
|
detect_cuda_support,
|
|
detect_vulkan_support,
|
|
)
|
|
|
|
self.assertIsNone(_prefer_discrete_vulkan_device())
|
|
detect_vulkan_support.cache_clear()
|
|
is_available, device_name = detect_vulkan_support()
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_name)
|
|
|
|
detect_cuda_support.cache_clear()
|
|
detect_compute_backend.cache_clear()
|
|
backend, _info = detect_compute_backend()
|
|
self.assertNotEqual(backend, ComputeBackend.VULKAN)
|
|
|
|
def test_prefer_discrete_with_noncontiguous_gpu_ids(self):
|
|
"""Preferred index is the Vulkan GPU id, not the list position."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel UHD 630\n"
|
|
"GPU2:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
_prefer_discrete_vulkan_device,
|
|
detect_vulkan_support,
|
|
)
|
|
|
|
preferred_idx = _prefer_discrete_vulkan_device()
|
|
self.assertEqual(preferred_idx, 2)
|
|
|
|
detect_vulkan_support.cache_clear()
|
|
is_available, device_name = detect_vulkan_support()
|
|
self.assertTrue(is_available)
|
|
self.assertEqual(device_name, "NVIDIA RTX 2060")
|
|
|
|
|
|
class TestDetectVulkanSupport(unittest.TestCase):
|
|
"""Test cases for detect_vulkan_support function."""
|
|
|
|
def setUp(self):
|
|
"""Clear lru_cache before each test."""
|
|
from vocalinux.utils.whispercpp_model_info import (
|
|
detect_compute_backend,
|
|
detect_cuda_support,
|
|
detect_vulkan_devices,
|
|
detect_vulkan_support,
|
|
)
|
|
|
|
detect_vulkan_support.cache_clear()
|
|
detect_vulkan_devices.cache_clear()
|
|
detect_cuda_support.cache_clear()
|
|
detect_compute_backend.cache_clear()
|
|
|
|
def test_detect_vulkan_support_returns_tuple(self):
|
|
"""Test that detect_vulkan_support returns a tuple."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
result = detect_vulkan_support()
|
|
self.assertIsInstance(result, tuple)
|
|
self.assertEqual(len(result), 2)
|
|
|
|
def test_detect_vulkan_support_when_available(self):
|
|
"""Test Vulkan detection when vulkaninfo is available."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = Intel Arc\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
is_available, device_name = detect_vulkan_support()
|
|
|
|
self.assertTrue(is_available)
|
|
self.assertEqual(device_name, "Intel Arc")
|
|
|
|
def test_detect_vulkan_support_prefers_discrete_on_hybrid(self):
|
|
"""Hybrid systems should report the discrete GPU name."""
|
|
vulkaninfo_output = (
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
|
|
"GPU1:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
|
|
)
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
is_available, device_name = detect_vulkan_support()
|
|
|
|
self.assertTrue(is_available)
|
|
self.assertEqual(device_name, "NVIDIA GeForce RTX 2060")
|
|
|
|
def test_compute_backend_stays_vulkan_with_real_summary_and_nvidia(self):
|
|
"""Regression: real GPU0: summary must not fall through to CUDA."""
|
|
vulkaninfo_output = (
|
|
"Devices:\n"
|
|
"========\n"
|
|
"GPU0:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU\n"
|
|
"\tdeviceName = Intel(R) UHD Graphics 630 (CFL GT2)\n"
|
|
"GPU1:\n"
|
|
"\tdeviceType = PHYSICAL_DEVICE_TYPE_DISCRETE_GPU\n"
|
|
"\tdeviceName = NVIDIA GeForce RTX 2060\n"
|
|
)
|
|
|
|
def run_side_effect(cmd, **kwargs):
|
|
if cmd and cmd[0] == "vulkaninfo":
|
|
return MagicMock(returncode=0, stdout=vulkaninfo_output)
|
|
if cmd and cmd[0] == "nvidia-smi":
|
|
return MagicMock(returncode=0, stdout="NVIDIA GeForce RTX 2060, 6144 MiB")
|
|
return MagicMock(returncode=1, stdout="")
|
|
|
|
with patch("subprocess.run", side_effect=run_side_effect):
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
|
|
|
backend, backend_info = detect_compute_backend()
|
|
self.assertEqual(backend, ComputeBackend.VULKAN)
|
|
self.assertEqual(backend_info, "NVIDIA GeForce RTX 2060")
|
|
|
|
def test_detect_vulkan_support_when_unavailable(self):
|
|
"""Test Vulkan detection when vulkaninfo is not available."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = FileNotFoundError("vulkaninfo not found")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
is_available, device_name = detect_vulkan_support()
|
|
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_name)
|
|
|
|
def test_detect_vulkan_support_on_timeout(self):
|
|
"""Test Vulkan detection when command times out."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = subprocess.TimeoutExpired("vulkaninfo", 5)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
is_available, device_name = detect_vulkan_support()
|
|
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_name)
|
|
|
|
def test_detect_vulkan_support_on_error(self):
|
|
"""Test Vulkan detection when command returns error."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=1, stdout="")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_vulkan_support
|
|
|
|
is_available, device_name = detect_vulkan_support()
|
|
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_name)
|
|
|
|
|
|
class TestDetectCudaSupport(unittest.TestCase):
|
|
"""Test cases for detect_cuda_support function."""
|
|
|
|
def test_detect_cuda_support_returns_tuple(self):
|
|
"""Test that detect_cuda_support returns a tuple."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
|
|
|
|
result = detect_cuda_support()
|
|
self.assertIsInstance(result, tuple)
|
|
self.assertEqual(len(result), 2)
|
|
|
|
def test_detect_cuda_support_when_available(self):
|
|
"""Test CUDA detection when nvidia-smi is available."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="NVIDIA RTX 3080, 10240 MiB\n")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
|
|
|
|
is_available, device_info = detect_cuda_support()
|
|
|
|
self.assertTrue(is_available)
|
|
self.assertIsNotNone(device_info)
|
|
self.assertIn("RTX 3080", device_info)
|
|
|
|
def test_detect_cuda_support_when_unavailable(self):
|
|
"""Test CUDA detection when nvidia-smi is not available."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = FileNotFoundError("nvidia-smi not found")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
|
|
|
|
is_available, device_info = detect_cuda_support()
|
|
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_info)
|
|
|
|
def test_detect_cuda_support_on_timeout(self):
|
|
"""Test CUDA detection when command times out."""
|
|
with patch("subprocess.run") as mock_run:
|
|
mock_run.side_effect = subprocess.TimeoutExpired("nvidia-smi", 5)
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cuda_support
|
|
|
|
is_available, device_info = detect_cuda_support()
|
|
|
|
self.assertFalse(is_available)
|
|
self.assertIsNone(device_info)
|
|
|
|
|
|
class TestDetectCPUInfo(unittest.TestCase):
|
|
"""Test cases for detect_cpu_info function."""
|
|
|
|
def test_detect_cpu_info_returns_string(self):
|
|
"""Test that detect_cpu_info returns a string."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
|
|
|
|
result = detect_cpu_info()
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
def test_detect_cpu_info_from_proc_cpuinfo(self):
|
|
"""Test CPU detection from /proc/cpuinfo."""
|
|
cpu_info_content = "model name : Intel(R) Core(TM) i7-10700K CPU @ 3.80GHz\n"
|
|
|
|
with patch("builtins.open", create=True) as mock_open:
|
|
mock_open.return_value.__enter__.return_value = iter([cpu_info_content])
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
|
|
|
|
result = detect_cpu_info()
|
|
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
def test_detect_cpu_info_fallback_to_nproc(self):
|
|
"""Test CPU detection falls back to nproc."""
|
|
with (
|
|
patch("builtins.open", create=True, side_effect=Exception("No /proc/cpuinfo")),
|
|
patch("subprocess.run") as mock_run,
|
|
):
|
|
|
|
mock_run.return_value = MagicMock(returncode=0, stdout="8\n")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
|
|
|
|
result = detect_cpu_info()
|
|
|
|
self.assertIsInstance(result, str)
|
|
|
|
def test_detect_cpu_info_default_fallback(self):
|
|
"""Test CPU detection returns default when all methods fail."""
|
|
with (
|
|
patch("builtins.open", create=True, side_effect=Exception()),
|
|
patch("subprocess.run", side_effect=Exception()),
|
|
):
|
|
|
|
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
|
|
|
|
result = detect_cpu_info()
|
|
|
|
self.assertEqual(result, "CPU")
|
|
|
|
|
|
class TestDetectComputeBackend(unittest.TestCase):
|
|
"""Test cases for detect_compute_backend function."""
|
|
|
|
def test_detect_compute_backend_returns_tuple(self):
|
|
"""Test that detect_compute_backend returns a tuple."""
|
|
from vocalinux.utils.whispercpp_model_info import detect_compute_backend
|
|
|
|
result = detect_compute_backend()
|
|
self.assertIsInstance(result, tuple)
|
|
self.assertEqual(len(result), 2)
|
|
|
|
def test_detect_compute_backend_prefers_vulkan(self):
|
|
"""Test that detect_compute_backend prefers Vulkan."""
|
|
with (
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
|
):
|
|
|
|
mock_vulkan.return_value = (True, "Intel Arc GPU")
|
|
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
|
|
|
backend, info = detect_compute_backend()
|
|
|
|
self.assertEqual(backend, ComputeBackend.VULKAN)
|
|
|
|
def test_detect_compute_backend_falls_back_to_cuda(self):
|
|
"""Test that detect_compute_backend falls back to CUDA."""
|
|
with (
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
|
):
|
|
|
|
mock_vulkan.return_value = (False, None)
|
|
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
|
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
|
|
|
backend, info = detect_compute_backend()
|
|
|
|
self.assertEqual(backend, ComputeBackend.CUDA)
|
|
|
|
def test_detect_compute_backend_falls_back_to_cpu(self):
|
|
"""Test that detect_compute_backend falls back to CPU."""
|
|
with (
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
|
patch("vocalinux.utils.whispercpp_model_info.detect_cpu_info") as mock_cpu,
|
|
):
|
|
|
|
mock_vulkan.return_value = (False, None)
|
|
mock_cuda.return_value = (False, None)
|
|
mock_cpu.return_value = "Intel Core i7"
|
|
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, detect_compute_backend
|
|
|
|
backend, info = detect_compute_backend()
|
|
|
|
self.assertEqual(backend, ComputeBackend.CPU)
|
|
|
|
|
|
class TestGetRecommendedModel(unittest.TestCase):
|
|
"""The recommendation is a ladder rung keyed to the installed build and free memory.
|
|
|
|
Spec #16 / ticket #20. Every reading is mocked: the installed build's
|
|
backend, free VRAM from nvidia-smi and free system RAM. Total memory is
|
|
never consulted, so no test here passes a total as a free reading.
|
|
"""
|
|
|
|
LADDER = {"large-v3-turbo-q8_0", "large-v3-turbo-q5_0", "small"}
|
|
|
|
def _recommend(self, build_backend, free_vram=None, free_ram_mib=10 * 1024.0):
|
|
from vocalinux.speech_recognition import model_ladder, recognition_manager
|
|
from vocalinux.utils import whispercpp_model_info as info
|
|
|
|
with (
|
|
patch.object(
|
|
recognition_manager,
|
|
"detect_pywhispercpp_gpu_backend",
|
|
return_value=build_backend,
|
|
),
|
|
patch.object(model_ladder, "free_vram_mib", return_value=free_vram),
|
|
patch.object(model_ladder, "free_system_ram_mib", return_value=free_ram_mib),
|
|
):
|
|
return info.get_recommended_model()
|
|
|
|
def test_returns_tuple(self):
|
|
model, reason = self._recommend("vulkan", free_vram=2600.0)
|
|
self.assertIsInstance(model, str)
|
|
self.assertIsInstance(reason, str)
|
|
|
|
def test_ample_free_vram_on_vulkan_build_selects_default_rung(self):
|
|
model, reason = self._recommend("vulkan", free_vram=2600.0)
|
|
self.assertEqual(model, "large-v3-turbo-q8_0")
|
|
self.assertIn("VRAM", reason)
|
|
|
|
def test_constrained_free_vram_steps_to_q5_rung(self):
|
|
model, reason = self._recommend("vulkan", free_vram=1500.0)
|
|
self.assertEqual(model, "large-v3-turbo-q5_0")
|
|
self.assertIn("1500 MiB free VRAM", reason)
|
|
|
|
def test_tight_free_vram_selects_floor(self):
|
|
model, _reason = self._recommend("vulkan", free_vram=900.0)
|
|
self.assertEqual(model, "small")
|
|
|
|
def test_total_vram_is_not_the_reading(self):
|
|
"""A 4 GB card whose VRAM is mostly taken by the desktop gets the floor."""
|
|
model, _reason = self._recommend("vulkan", free_vram=512.0)
|
|
self.assertEqual(model, "small")
|
|
|
|
def test_integrated_graphics_read_free_ram(self):
|
|
"""No NVIDIA reading means the Vulkan build keeps its model in system RAM."""
|
|
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=10 * 1024.0)
|
|
self.assertEqual(model, "large-v3-turbo-q8_0")
|
|
self.assertIn("RAM", reason)
|
|
|
|
def test_cpu_build_ignores_a_visible_gpu_and_reads_ram(self):
|
|
"""A CPU-only build never runs on the GPU, so VRAM is not the constraint."""
|
|
model, reason = self._recommend("cpu", free_vram=2600.0, free_ram_mib=1200.0)
|
|
self.assertEqual(model, "small")
|
|
self.assertIn("RAM", reason)
|
|
self.assertNotIn("VRAM", reason)
|
|
|
|
def test_keys_to_the_installed_build_not_the_hardware_probe(self):
|
|
"""The informational probe reporting CPU must not hide an installed GPU build."""
|
|
from vocalinux.speech_recognition import model_ladder, recognition_manager
|
|
from vocalinux.utils import whispercpp_model_info as info
|
|
|
|
with (
|
|
patch.object(
|
|
info, "detect_compute_backend", return_value=(info.ComputeBackend.CPU, "x")
|
|
),
|
|
patch.object(
|
|
recognition_manager,
|
|
"detect_pywhispercpp_gpu_backend",
|
|
return_value="vulkan",
|
|
),
|
|
patch.object(model_ladder, "free_vram_mib", return_value=2600.0),
|
|
patch.object(model_ladder, "free_system_ram_mib", return_value=512.0),
|
|
):
|
|
model, reason = info.get_recommended_model()
|
|
self.assertEqual(model, "large-v3-turbo-q8_0")
|
|
self.assertIn("VRAM", reason)
|
|
|
|
def test_unreadable_memory_recommends_the_floor(self):
|
|
model, reason = self._recommend("vulkan", free_vram=None, free_ram_mib=None)
|
|
self.assertEqual(model, "small")
|
|
self.assertIn("unreadable", reason)
|
|
|
|
def test_recommendation_stays_inside_the_ladder_for_any_reading(self):
|
|
"""Unquantized turbo, the large family and medium never come back as a default."""
|
|
for backend in ("vulkan", "cuda", "cpu"):
|
|
for free_vram in (None, 0.0, 900.0, 1500.0, 2600.0, 8000.0):
|
|
for free_ram in (512.0, 1200.0, 4096.0, 16384.0):
|
|
model, _reason = self._recommend(backend, free_vram, free_ram)
|
|
self.assertIn(model, self.LADDER, (backend, free_vram, free_ram))
|
|
|
|
def test_reason_names_the_backend_the_build_has(self):
|
|
_model, reason = self._recommend("cuda", free_vram=2600.0)
|
|
self.assertIn("cuda build", reason)
|
|
|
|
|
|
class TestGetModelPath(unittest.TestCase):
|
|
"""Test cases for get_model_path function."""
|
|
|
|
def test_get_model_path_returns_string(self):
|
|
"""Test that get_model_path returns a string."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
result = get_model_path("tiny")
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
def test_get_model_path_tiny(self):
|
|
"""Test get_model_path for tiny model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("tiny")
|
|
self.assertIn("ggml-tiny.bin", path)
|
|
|
|
def test_get_model_path_base(self):
|
|
"""Test get_model_path for base model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("base")
|
|
self.assertIn("ggml-base.bin", path)
|
|
|
|
def test_get_model_path_small(self):
|
|
"""Test get_model_path for small model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("small")
|
|
self.assertIn("ggml-small.bin", path)
|
|
|
|
def test_get_model_path_medium(self):
|
|
"""Test get_model_path for medium model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("medium")
|
|
self.assertIn("ggml-medium.bin", path)
|
|
|
|
def test_get_model_path_large_uses_v3(self):
|
|
"""Test get_model_path for large model uses v3 variant."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("large")
|
|
self.assertIn("ggml-large-v3.bin", path)
|
|
|
|
def test_large_model_url_uses_v3(self):
|
|
"""Test large model metadata points to upstream large-v3."""
|
|
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
|
|
|
|
self.assertIn("ggml-large-v3.bin", WHISPERCPP_MODEL_INFO["large"]["url"])
|
|
|
|
def test_get_model_path_large_v3_turbo(self):
|
|
"""Test get_model_path for large-v3-turbo model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("large-v3-turbo")
|
|
self.assertIn("ggml-large-v3-turbo.bin", path)
|
|
|
|
def test_get_model_path_quantized_model(self):
|
|
"""Test get_model_path for quantized model."""
|
|
from vocalinux.utils.whispercpp_model_info import get_model_path
|
|
|
|
path = get_model_path("large-v3-turbo-q5_0")
|
|
self.assertIn("ggml-large-v3-turbo-q5_0.bin", path)
|
|
|
|
|
|
class TestIsModelDownloaded(unittest.TestCase):
|
|
"""Test cases for is_model_downloaded function."""
|
|
|
|
def test_is_model_downloaded_returns_bool(self):
|
|
"""Test that is_model_downloaded returns a boolean."""
|
|
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
|
|
|
|
result = is_model_downloaded("tiny")
|
|
self.assertIsInstance(result, bool)
|
|
|
|
def test_is_model_downloaded_when_exists(self):
|
|
"""Test is_model_downloaded when model file exists."""
|
|
with patch("os.path.exists", return_value=True):
|
|
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
|
|
|
|
result = is_model_downloaded("base")
|
|
self.assertTrue(result)
|
|
|
|
def test_is_model_downloaded_when_not_exists(self):
|
|
"""Test is_model_downloaded when model file does not exist."""
|
|
with patch("os.path.exists", return_value=False):
|
|
from vocalinux.utils.whispercpp_model_info import is_model_downloaded
|
|
|
|
result = is_model_downloaded("base")
|
|
self.assertFalse(result)
|
|
|
|
|
|
class TestGetBackendDisplayName(unittest.TestCase):
|
|
"""Test cases for get_backend_display_name function."""
|
|
|
|
def test_get_backend_display_name_returns_string(self):
|
|
"""Test that get_backend_display_name returns a string."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
|
|
|
result = get_backend_display_name(ComputeBackend.CPU)
|
|
self.assertIsInstance(result, str)
|
|
self.assertGreater(len(result), 0)
|
|
|
|
def test_get_backend_display_name_vulkan(self):
|
|
"""Test display name for Vulkan backend."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
|
|
|
result = get_backend_display_name(ComputeBackend.VULKAN)
|
|
self.assertEqual(result, "Vulkan GPU")
|
|
|
|
def test_get_backend_display_name_cuda(self):
|
|
"""Test display name for CUDA backend."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
|
|
|
result = get_backend_display_name(ComputeBackend.CUDA)
|
|
self.assertEqual(result, "NVIDIA CUDA")
|
|
|
|
def test_get_backend_display_name_cpu(self):
|
|
"""Test display name for CPU backend."""
|
|
from vocalinux.utils.whispercpp_model_info import ComputeBackend, get_backend_display_name
|
|
|
|
result = get_backend_display_name(ComputeBackend.CPU)
|
|
self.assertEqual(result, "CPU")
|
|
|
|
def test_get_backend_display_name_unknown(self):
|
|
"""Test display name for unknown backend."""
|
|
from vocalinux.utils.whispercpp_model_info import get_backend_display_name
|
|
|
|
result = get_backend_display_name("rocm")
|
|
self.assertEqual(result, "ROCM")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|