Files
voculinux/tests/test_silero_vad.py
soubarnak ea7be5f111 feat(identity): rename user-visible identity to voculinux with one-time legacy import
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
2026-11-06 01:29:39 +05:30

244 lines
8.4 KiB
Python

"""
Tests for Silero VAD wrapper (silero_vad.py).
Covers:
- SileroVAD.process() return type and validation
- SileroVAD.reset() state clearing
- load_silero_vad() success and graceful fallback
- Sensitivity-to-threshold mapping
"""
import sys
from unittest.mock import MagicMock, patch # noqa: E402
# Earlier test modules install `sys.modules["numpy"] = MagicMock()` at module
# load and don't restore it. Reuse the real module cached by conftest instead
# of unloading/re-importing NumPy's compiled extensions.
if isinstance(sys.modules.get("numpy"), MagicMock):
_real_numpy = getattr(sys, "_voculinux_real_numpy", None)
if _real_numpy is not None:
sys.modules["numpy"] = _real_numpy
import numpy as np # noqa: E402
import pytest # noqa: E402
from vocalinux.speech_recognition import silero_vad as _sv_mod # noqa: E402
from vocalinux.speech_recognition.silero_vad import ( # noqa: E402
_CONTEXT_SIZE,
SILERO_CHUNK_SIZE,
SILERO_SAMPLE_RATE,
SileroVAD,
is_silero_available,
load_silero_vad,
)
# silero_vad imported numpy when first loaded (likely with a mocked numpy
# reference). Point its module-level `np` at the real numpy so SileroVAD
# methods use real array operations.
_sv_mod.np = np
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_mock_session(prob=0.05):
"""Create a mock ONNX InferenceSession mimicking Silero VAD output."""
session = MagicMock()
state = np.zeros((2, 1, 128), dtype=np.float32)
session.run.return_value = ([[prob]], state)
return session
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
class TestSileroVADProcess:
"""Test the process() method."""
def test_returns_float(self):
vad = SileroVAD.__new__(SileroVAD)
vad._session = _make_mock_session()
vad._state = np.zeros((2, 1, 128), dtype=np.float32)
vad._context = np.zeros((1, _CONTEXT_SIZE), dtype=np.float32)
vad._sr = np.array(SILERO_SAMPLE_RATE, dtype=np.int64)
chunk = np.zeros(SILERO_CHUNK_SIZE, dtype=np.int16)
prob = vad.process(chunk)
assert isinstance(prob, float)
def test_prob_from_session(self):
vad = SileroVAD.__new__(SileroVAD)
vad._session = _make_mock_session(prob=0.42)
vad._state = np.zeros((2, 1, 128), dtype=np.float32)
vad._context = np.zeros((1, _CONTEXT_SIZE), dtype=np.float32)
vad._sr = np.array(SILERO_SAMPLE_RATE, dtype=np.int64)
chunk = np.zeros(SILERO_CHUNK_SIZE, dtype=np.int16)
prob = vad.process(chunk)
assert abs(prob - 0.42) < 1e-6
def test_session_receives_correct_input_shape(self):
"""Session should receive (1, 512+64) float32 input."""
session = _make_mock_session()
vad = SileroVAD.__new__(SileroVAD)
vad._session = session
vad._state = np.zeros((2, 1, 128), dtype=np.float32)
vad._context = np.zeros((1, _CONTEXT_SIZE), dtype=np.float32)
vad._sr = np.array(SILERO_SAMPLE_RATE, dtype=np.int64)
chunk = np.zeros(SILERO_CHUNK_SIZE, dtype=np.int16)
vad.process(chunk)
call_kwargs = session.run.call_args
input_tensor = call_kwargs[1]["input"] if call_kwargs[1] else call_kwargs[0][1]["input"]
assert input_tensor.shape == (1, SILERO_CHUNK_SIZE + _CONTEXT_SIZE)
assert input_tensor.dtype == np.float32
def test_wrong_length_raises_value_error(self):
vad = SileroVAD.__new__(SileroVAD)
vad._session = _make_mock_session()
vad._state = np.zeros((2, 1, 128), dtype=np.float32)
vad._context = np.zeros((1, _CONTEXT_SIZE), dtype=np.float32)
vad._sr = np.array(SILERO_SAMPLE_RATE, dtype=np.int64)
with pytest.raises(ValueError, match="Expected 512"):
vad.process(np.zeros(256, dtype=np.int16))
with pytest.raises(ValueError, match="Expected 512"):
vad.process(np.zeros(1024, dtype=np.int16))
def test_context_updated_after_process(self):
"""Context should contain last 64 samples of the processed chunk."""
vad = SileroVAD.__new__(SileroVAD)
vad._session = _make_mock_session()
vad._state = np.zeros((2, 1, 128), dtype=np.float32)
vad._context = np.zeros((1, _CONTEXT_SIZE), dtype=np.float32)
vad._sr = np.array(SILERO_SAMPLE_RATE, dtype=np.int64)
chunk = np.arange(SILERO_CHUNK_SIZE, dtype=np.int16)
vad.process(chunk)
expected = chunk[-_CONTEXT_SIZE:].astype(np.float32) / 32768.0
assert np.allclose(vad._context[0], expected)
class TestSileroVADReset:
"""Test state reset between sessions."""
def test_reset_clears_state(self):
vad = SileroVAD.__new__(SileroVAD)
vad._state = np.ones((2, 1, 128), dtype=np.float32)
vad._context = np.ones((1, _CONTEXT_SIZE), dtype=np.float32)
vad.reset()
assert np.allclose(vad._state, 0.0)
assert np.allclose(vad._context, 0.0)
def test_reset_preserves_correct_shapes(self):
vad = SileroVAD.__new__(SileroVAD)
vad._state = np.ones((2, 1, 128), dtype=np.float32)
vad._context = np.ones((1, _CONTEXT_SIZE), dtype=np.float32)
vad.reset()
assert vad._state.shape == (2, 1, 128)
assert vad._context.shape == (1, _CONTEXT_SIZE)
class TestSensitivityMapping:
"""Test the sensitivity -> threshold mapping used in recognition_manager."""
@pytest.mark.parametrize(
"sensitivity,expected_threshold",
[
(1, 0.800),
(2, 0.675),
(3, 0.550),
(4, 0.425),
(5, 0.300),
],
)
def test_mapping(self, sensitivity, expected_threshold):
threshold = 0.8 - (sensitivity - 1) * 0.125
assert abs(threshold - expected_threshold) < 1e-10
class TestConstants:
"""Test module constants."""
def test_chunk_size(self):
assert SILERO_CHUNK_SIZE == 512
def test_sample_rate(self):
assert SILERO_SAMPLE_RATE == 16000
def test_context_size(self):
assert _CONTEXT_SIZE == 64
class TestLoadFallback:
"""Test graceful fallback when onnxruntime is unavailable."""
def test_load_returns_none_on_import_error(self):
"""If SileroVAD() raises ImportError, load_silero_vad returns None."""
with patch(
"vocalinux.speech_recognition.silero_vad.SileroVAD",
side_effect=ImportError("no onnxruntime"),
):
result = load_silero_vad()
assert result is None
def test_load_returns_none_on_runtime_error(self):
"""If SileroVAD() raises RuntimeError (bad model), load returns None."""
with patch(
"vocalinux.speech_recognition.silero_vad.SileroVAD",
side_effect=RuntimeError("corrupted model"),
):
result = load_silero_vad()
assert result is None
def test_load_returns_vad_on_success(self):
"""load_silero_vad() returns a SileroVAD when init succeeds."""
mock_vad = MagicMock(spec=SileroVAD)
with patch(
"vocalinux.speech_recognition.silero_vad.SileroVAD",
return_value=mock_vad,
):
result = load_silero_vad()
assert result is mock_vad
class TestIsSileroAvailable:
"""Cheap probe used by Settings UI to decide whether to show the install hint."""
def test_returns_false_without_onnxruntime(self):
with patch.dict(sys.modules, {"onnxruntime": None}):
assert is_silero_available() is False
def test_returns_false_when_model_missing(self):
# onnxruntime importable, but model file gone
ort_mod = MagicMock()
with (
patch.dict(sys.modules, {"onnxruntime": ort_mod}),
patch(
"vocalinux.speech_recognition.silero_vad.os.path.exists",
return_value=False,
),
):
assert is_silero_available() is False
def test_returns_true_when_both_present(self):
ort_mod = MagicMock()
with (
patch.dict(sys.modules, {"onnxruntime": ort_mod}),
patch(
"vocalinux.speech_recognition.silero_vad.os.path.exists",
return_value=True,
),
):
assert is_silero_available() is True