fix(whispercpp): skip unsupported context_params on pywhispercpp 1.4 (#626)

* fix(whispercpp): skip unsupported context_params on pywhispercpp 1.4

Fixes #625

* test(whispercpp): cover context_params signature inspection failure
This commit is contained in:
Jatin K Malik
2026-08-02 19:41:12 -07:00
committed by GitHub
parent ced8620e69
commit 2152dbb90d
2 changed files with 111 additions and 24 deletions
@@ -7,6 +7,7 @@ currently supporting VOSK, Whisper, and whisper.cpp.
import ctypes
import importlib.util
import inspect
import json
import logging
import os
@@ -1298,11 +1299,26 @@ class SpeechRecognitionManager:
compatible_kwargs = self._filter_whispercpp_model_kwargs(model_kwargs)
if gpu_device is not None and gpu_device >= 0:
compatible_kwargs["context_params"] = {"gpu_device": gpu_device}
try:
supports_context_params = (
"context_params" in inspect.signature(Model.__init__).parameters
)
except (TypeError, ValueError) as exc:
supports_context_params = False
logger.debug(f"Could not inspect pywhispercpp Model signature: {exc}")
if supports_context_params:
compatible_kwargs["context_params"] = {"gpu_device": gpu_device}
else:
logger.warning(
"pywhispercpp does not support context_params; "
"upgrade pywhispercpp to enable GPU device selection. "
"Falling back to default device."
)
try:
return Model(model_path, **compatible_kwargs)
except TypeError as exc:
except (TypeError, AttributeError) as exc:
# Older pywhispercpp releases (< ~1.4.0) do not accept context_params.
# Retry without GPU device selection so the engine still loads.
if "context_params" in compatible_kwargs and (
+93 -22
View File
@@ -785,15 +785,43 @@ class TestWhispercppInitialization(unittest.TestCase):
class TestWhispercppGpuDeviceSelection(unittest.TestCase):
"""Test whisper.cpp GPU device selection logic."""
class ContextParamsModel:
calls = []
def __init__(self, model_path, context_params=None, **kwargs):
call_kwargs = dict(kwargs)
if context_params is not None:
call_kwargs["context_params"] = context_params
self.__class__.calls.append((model_path, call_kwargs))
class OldSignatureModel:
calls = []
def __init__(self, model_path, **kwargs):
self.__class__.calls.append((model_path, dict(kwargs)))
class AttributeErrorContextParamsModel:
calls = []
def __init__(self, model_path, context_params=None, **kwargs):
call_kwargs = dict(kwargs)
if context_params is not None:
call_kwargs["context_params"] = context_params
self.__class__.calls.append((model_path, call_kwargs))
if context_params is not None:
raise AttributeError(
"'whisper_full_params' object has no attribute 'context_params'"
)
def test_gpu_device_auto_select_discrete(self):
"""Test auto-selection of discrete GPU when configured as None."""
manager = _make_manager(engine="whisper_cpp", whispercpp_gpu_device=None)
manager.model_size = "tiny"
mock_pywhispercpp = MagicMock()
mock_model = MagicMock()
mock_pywhispercpp.Model = MagicMock(return_value=mock_model)
mock_pywhispercpp.model.Model = MagicMock(return_value=mock_model)
self.ContextParamsModel.calls = []
mock_pywhispercpp.Model = self.ContextParamsModel
mock_pywhispercpp.model.Model = self.ContextParamsModel
mock_psutil = MagicMock()
mock_psutil.virtual_memory.return_value.total = 8 * 1024 * 1024 * 1024
@@ -843,7 +871,7 @@ class TestWhispercppGpuDeviceSelection(unittest.TestCase):
return_value=1,
):
manager._init_whispercpp()
assert mock_pywhispercpp.Model.call_args.kwargs.get(
assert self.ContextParamsModel.calls[-1][1].get(
"context_params"
) == {"gpu_device": 1}
@@ -853,9 +881,9 @@ class TestWhispercppGpuDeviceSelection(unittest.TestCase):
manager.model_size = "tiny"
mock_pywhispercpp = MagicMock()
mock_model = MagicMock()
mock_pywhispercpp.Model = MagicMock(return_value=mock_model)
mock_pywhispercpp.model.Model = MagicMock(return_value=mock_model)
self.ContextParamsModel.calls = []
mock_pywhispercpp.Model = self.ContextParamsModel
mock_pywhispercpp.model.Model = self.ContextParamsModel
mock_psutil = MagicMock()
mock_psutil.virtual_memory.return_value.total = 8 * 1024 * 1024 * 1024
@@ -900,24 +928,18 @@ class TestWhispercppGpuDeviceSelection(unittest.TestCase):
return_value=vulkan_devices,
):
manager._init_whispercpp()
assert mock_pywhispercpp.Model.call_args.kwargs.get(
assert self.ContextParamsModel.calls[-1][1].get(
"context_params"
) == {"gpu_device": 0}
def test_gpu_device_context_params_fallback_on_old_pywhispercpp(self):
"""Test graceful fallback when pywhispercpp rejects context_params."""
def test_gpu_device_skips_context_params_for_old_pywhispercpp_signature(self):
"""Old pywhispercpp signatures must not receive context_params."""
manager = _make_manager(engine="whisper_cpp", whispercpp_gpu_device=1)
manager.model_size = "tiny"
mock_pywhispercpp = MagicMock()
mock_model = MagicMock()
def _model_side_effect(*args, **kwargs):
if kwargs.get("context_params"):
raise TypeError("Model() got an unexpected keyword argument 'context_params'")
return mock_model
mock_pywhispercpp.Model.side_effect = _model_side_effect
self.OldSignatureModel.calls = []
mock_pywhispercpp.Model = self.OldSignatureModel
with patch.dict(
"sys.modules",
@@ -927,12 +949,61 @@ class TestWhispercppGpuDeviceSelection(unittest.TestCase):
},
):
result = manager._load_model_with_compatible_params("/fake/model.bin", {}, gpu_device=1)
assert result is mock_model
assert mock_pywhispercpp.Model.call_count == 2
assert mock_pywhispercpp.Model.call_args_list[0].kwargs.get("context_params") == {
assert isinstance(result, self.OldSignatureModel)
assert len(self.OldSignatureModel.calls) == 1
assert "context_params" not in self.OldSignatureModel.calls[0][1]
def test_gpu_device_skips_context_params_when_signature_inspection_fails(self):
"""If Model.__init__ cannot be inspected, skip context_params safely."""
manager = _make_manager(engine="whisper_cpp", whispercpp_gpu_device=1)
manager.model_size = "tiny"
mock_pywhispercpp = MagicMock()
self.OldSignatureModel.calls = []
mock_pywhispercpp.Model = self.OldSignatureModel
with patch.dict(
"sys.modules",
{
"pywhispercpp": mock_pywhispercpp,
"pywhispercpp.model": mock_pywhispercpp,
},
):
with patch(
"vocalinux.speech_recognition.recognition_manager.inspect.signature",
side_effect=ValueError("no signature"),
):
result = manager._load_model_with_compatible_params(
"/fake/model.bin", {}, gpu_device=1
)
assert isinstance(result, self.OldSignatureModel)
assert len(self.OldSignatureModel.calls) == 1
assert "context_params" not in self.OldSignatureModel.calls[0][1]
def test_gpu_device_context_params_fallback_on_attribute_error(self):
"""AttributeError from pywhispercpp context_params is retried without it."""
manager = _make_manager(engine="whisper_cpp", whispercpp_gpu_device=1)
manager.model_size = "tiny"
mock_pywhispercpp = MagicMock()
self.AttributeErrorContextParamsModel.calls = []
mock_pywhispercpp.Model = self.AttributeErrorContextParamsModel
with patch.dict(
"sys.modules",
{
"pywhispercpp": mock_pywhispercpp,
"pywhispercpp.model": mock_pywhispercpp,
},
):
result = manager._load_model_with_compatible_params("/fake/model.bin", {}, gpu_device=1)
assert isinstance(result, self.AttributeErrorContextParamsModel)
assert len(self.AttributeErrorContextParamsModel.calls) == 2
assert self.AttributeErrorContextParamsModel.calls[0][1].get("context_params") == {
"gpu_device": 1
}
assert "context_params" not in mock_pywhispercpp.Model.call_args_list[1].kwargs
assert "context_params" not in self.AttributeErrorContextParamsModel.calls[1][1]
def test_gpu_device_not_set_for_cpu_backend(self):
"""Test that GPU device selection is skipped on CPU backend."""