From e4bc6c81b53586c3be43f14802b835c366fda217 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Mon, 27 Jul 2026 01:52:08 -0400 Subject: [PATCH 1/3] fix(s2s): validate SileroVAD detection threshold Reject non-finite and out-of-range thresholds before loading the model. --- src/rai_s2s/rai_s2s/asr/models/silero_vad.py | 9 ++++++++ tests/s2s/__init__.py | 0 tests/s2s/test_silero_vad_threshold.py | 23 ++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 tests/s2s/__init__.py create mode 100644 tests/s2s/test_silero_vad_threshold.py diff --git a/src/rai_s2s/rai_s2s/asr/models/silero_vad.py b/src/rai_s2s/rai_s2s/asr/models/silero_vad.py index 8599a24ad..9e9119358 100644 --- a/src/rai_s2s/rai_s2s/asr/models/silero_vad.py +++ b/src/rai_s2s/rai_s2s/asr/models/silero_vad.py @@ -59,6 +59,15 @@ class SileroVAD(BaseVoiceDetectionModel): def __init__(self, sampling_rate: Literal[8000, 16000] = 16000, threshold=0.5): super(SileroVAD, self).__init__() + try: + thr = float(threshold) + except (TypeError, ValueError) as e: + raise ValueError("threshold must be a finite number") from e + if thr != thr or thr in (float("inf"), float("-inf")): # NaN/inf + raise ValueError("threshold must be a finite number") + if not (0.0 < thr <= 1.0): + raise ValueError("threshold must be in the interval (0, 1]") + threshold = thr self.model_name = "silero_vad" self.model, _ = torch.hub.load( repo_or_dir="snakers4/silero-vad", diff --git a/tests/s2s/__init__.py b/tests/s2s/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/s2s/test_silero_vad_threshold.py b/tests/s2s/test_silero_vad_threshold.py new file mode 100644 index 000000000..9371472ff --- /dev/null +++ b/tests/s2s/test_silero_vad_threshold.py @@ -0,0 +1,23 @@ +# Copyright (C) 2026 +import math +from unittest.mock import patch + +import pytest + + +def test_silero_vad_rejects_bad_threshold_before_hub_load(): + import rai_s2s.asr.models.silero_vad as mod + + def boom(*a, **k): + raise AssertionError("torch.hub.load should not run for invalid threshold") + + with patch.object(mod.torch.hub, "load", side_effect=boom): + with pytest.raises(ValueError, match="threshold"): + mod.SileroVAD(threshold=0) + with pytest.raises(ValueError, match="threshold"): + mod.SileroVAD(threshold=-0.1) + with pytest.raises(ValueError, match="threshold"): + netto = float("nan") + mod.SileroVAD(threshold=netto) + with pytest.raises(ValueError, match="threshold"): + mod.SileroVAD(threshold=1.5) From 28f389e17459fece5405ef0a728221ee80b8d344 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Mon, 27 Jul 2026 01:52:43 -0400 Subject: [PATCH 2/3] test(s2s): load SileroVAD module without package side effects --- tests/s2s/test_silero_vad_threshold.py | 46 +++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tests/s2s/test_silero_vad_threshold.py b/tests/s2s/test_silero_vad_threshold.py index 9371472ff..476d3b5c0 100644 --- a/tests/s2s/test_silero_vad_threshold.py +++ b/tests/s2s/test_silero_vad_threshold.py @@ -1,12 +1,51 @@ # Copyright (C) 2026 +import importlib.util import math -from unittest.mock import patch +import sys +from pathlib import Path +from unittest.mock import MagicMock, patch import pytest +ROOT = Path(__file__).resolve().parents[2] +MOD_PATH = ROOT / "src/rai_s2s/rai_s2s/asr/models/silero_vad.py" + + +def _load_silero_module(): + # Stub heavy package imports used only at module level. + sys.modules.setdefault("torch", MagicMock()) + sys.modules.setdefault("numpy", MagicMock()) + # base model path + base_path = ROOT / "src/rai_s2s/rai_s2s/asr/models/base.py" + # Ensure package stubs + for name in [ + "rai_s2s", + "rai_s2s.asr", + "rai_s2s.asr.models", + ]: + if name not in sys.modules: + sys.modules[name] = MagicMock() + # Load real base + spec_b = importlib.util.spec_from_file_location("rai_s2s.asr.models.base", base_path) + base = importlib.util.module_from_spec(spec_b) + assert spec_b and spec_b.loader + # Minimal stubs for base imports + sys.modules["numpy.typing"] = MagicMock() + spec_b.loader.exec_module(base) + sys.modules["rai_s2s.asr.models.base"] = base + sys.modules["rai_s2s.asr.models"].BaseVoiceDetectionModel = base.BaseVoiceDetectionModel + + spec = importlib.util.spec_from_file_location( + "rai_s2s.asr.models.silero_vad", MOD_PATH + ) + mod = importlib.util.module_from_spec(spec) + assert spec and spec.loader + spec.loader.exec_module(mod) + return mod + def test_silero_vad_rejects_bad_threshold_before_hub_load(): - import rai_s2s.asr.models.silero_vad as mod + mod = _load_silero_module() def boom(*a, **k): raise AssertionError("torch.hub.load should not run for invalid threshold") @@ -17,7 +56,6 @@ def boom(*a, **k): with pytest.raises(ValueError, match="threshold"): mod.SileroVAD(threshold=-0.1) with pytest.raises(ValueError, match="threshold"): - netto = float("nan") - mod.SileroVAD(threshold=netto) + mod.SileroVAD(threshold=float("nan")) with pytest.raises(ValueError, match="threshold"): mod.SileroVAD(threshold=1.5) From 57b867a5a41d2562c1e404112f1c23fc5e6d6ad8 Mon Sep 17 00:00:00 2001 From: Bartok9 Date: Mon, 27 Jul 2026 01:53:29 -0400 Subject: [PATCH 3/3] test(s2s): harden SileroVAD threshold unit test --- tests/s2s/test_silero_vad_threshold.py | 70 ++++++++++++++++++-------- 1 file changed, 48 insertions(+), 22 deletions(-) diff --git a/tests/s2s/test_silero_vad_threshold.py b/tests/s2s/test_silero_vad_threshold.py index 476d3b5c0..2ecc8e98d 100644 --- a/tests/s2s/test_silero_vad_threshold.py +++ b/tests/s2s/test_silero_vad_threshold.py @@ -1,7 +1,7 @@ # Copyright (C) 2026 import importlib.util -import math import sys +import types from pathlib import Path from unittest.mock import MagicMock, patch @@ -9,38 +9,64 @@ ROOT = Path(__file__).resolve().parents[2] MOD_PATH = ROOT / "src/rai_s2s/rai_s2s/asr/models/silero_vad.py" +BASE_PATH = ROOT / "src/rai_s2s/rai_s2s/asr/models/base.py" + + +def _ensure_numpy(): + try: + import numpy # noqa: F401 + return + except Exception: + pass + np = types.ModuleType("numpy") + np.__path__ = [] # type: ignore + npt = types.ModuleType("numpy.typing") + npt.NDArray = object + sys.modules["numpy"] = np + sys.modules["numpy.typing"] = npt def _load_silero_module(): - # Stub heavy package imports used only at module level. - sys.modules.setdefault("torch", MagicMock()) - sys.modules.setdefault("numpy", MagicMock()) - # base model path - base_path = ROOT / "src/rai_s2s/rai_s2s/asr/models/base.py" - # Ensure package stubs - for name in [ - "rai_s2s", - "rai_s2s.asr", - "rai_s2s.asr.models", - ]: + _ensure_numpy() + torch = MagicMock() + sys.modules["torch"] = torch + for name in ["rai_s2s", "rai_s2s.asr", "rai_s2s.asr.models"]: if name not in sys.modules: - sys.modules[name] = MagicMock() - # Load real base - spec_b = importlib.util.spec_from_file_location("rai_s2s.asr.models.base", base_path) + m = types.ModuleType(name) + m.__path__ = [] # type: ignore + sys.modules[name] = m + spec_b = importlib.util.spec_from_file_location("base_under_test_silero", BASE_PATH) base = importlib.util.module_from_spec(spec_b) assert spec_b and spec_b.loader - # Minimal stubs for base imports - sys.modules["numpy.typing"] = MagicMock() spec_b.loader.exec_module(base) - sys.modules["rai_s2s.asr.models.base"] = base sys.modules["rai_s2s.asr.models"].BaseVoiceDetectionModel = base.BaseVoiceDetectionModel + # patch import target used by silero + import rai_s2s.asr.models as models_pkg # type: ignore + models_pkg.BaseVoiceDetectionModel = base.BaseVoiceDetectionModel - spec = importlib.util.spec_from_file_location( - "rai_s2s.asr.models.silero_vad", MOD_PATH - ) + # Also satisfy "from rai_s2s.asr.models import BaseVoiceDetectionModel" + # by ensuring submodule package has attribute (already) + spec = importlib.util.spec_from_file_location("silero_vad_under_test", MOD_PATH) mod = importlib.util.module_from_spec(spec) assert spec and spec.loader - spec.loader.exec_module(mod) + # Pre-insert parent modules so relative-style imports via package path work + sys.modules["rai_s2s.asr.models.silero_vad"] = mod + # Make import try find BaseVoiceDetectionModel - monkeypatch importlib + import builtins + real_import = builtins.__import__ + + def guarded(name, globals=None, locals=None, fromlist=(), level=0): + if name == "rai_s2s.asr.models" or name.endswith("asr.models"): + m = sys.modules["rai_s2s.asr.models"] + return m + return real_import(name, globals, locals, fromlist, level) + + builtins.__import__ = guarded + try: + spec.loader.exec_module(mod) + finally: + builtins.__import__ = real_import + mod.torch = torch return mod