diff --git a/src/rai_s2s/pyproject.toml b/src/rai_s2s/pyproject.toml index d89a779dd..fe0734725 100644 --- a/src/rai_s2s/pyproject.toml +++ b/src/rai_s2s/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "rai_s2s" -version = "1.0.1" +version = "1.0.2" description = "Speech-to-Speech module for RAI framework" readme = "README.md" requires-python = ">=3.10,<3.13" diff --git a/src/rai_s2s/rai_s2s/asr/agents/asr_agent.py b/src/rai_s2s/rai_s2s/asr/agents/asr_agent.py index 53c3328e4..c73e7a213 100644 --- a/src/rai_s2s/rai_s2s/asr/agents/asr_agent.py +++ b/src/rai_s2s/rai_s2s/asr/agents/asr_agent.py @@ -30,7 +30,7 @@ ) from typing_extensions import Self -from rai_s2s.asr.agents.initialization import load_config +from rai_s2s.asr.agents.initialization import TRANSCRIBE_MODELS, load_config from rai_s2s.asr.models import BaseTranscriptionModel, BaseVoiceDetectionModel from rai_s2s.sound_device import ( SoundDeviceConfig, @@ -121,32 +121,39 @@ def from_config(cls, cfg_path: Optional[str] = None) -> Self: is_output=False, ) match cfg.transcribe.model_type: - case "LocalWhisper (Free)": + case "LocalWhisper": from rai_s2s.asr.models import LocalWhisper model = LocalWhisper( cfg.transcribe.model_name, 16000, language=cfg.transcribe.language ) - case "FasterWhisper (Free)": + case "FasterWhisper": from rai_s2s.asr.models import FasterWhisper model = FasterWhisper( cfg.transcribe.model_name, 16000, language=cfg.transcribe.language ) - case "OpenAI (Cloud)": + case "OpenAI": from rai_s2s.asr.models import OpenAIWhisper model = OpenAIWhisper( cfg.transcribe.model_name, 16000, language=cfg.transcribe.language ) case _: - raise ValueError(f"Unknown model name f{cfg.transcribe.model_name}") + raise ValueError( + f"Unknown transcription model: {cfg.transcribe.model_type}. " + f"Must be one of {TRANSCRIBE_MODELS}" + ) match cfg.voice_activity_detection.model_name: case "SileroVAD": from rai_s2s.asr.models import SileroVAD vad = SileroVAD(16000, cfg.voice_activity_detection.threshold) + case _: + raise ValueError( + f"Unknown VAD model: {cfg.voice_activity_detection.model_name}" + ) agent = cls(microphone_configuration, "rai_auto_asr_agent", model, vad) if cfg.wakeword.is_used: diff --git a/tests/s2s/test_asr_agent_from_config.py b/tests/s2s/test_asr_agent_from_config.py new file mode 100644 index 000000000..da82d66f4 --- /dev/null +++ b/tests/s2s/test_asr_agent_from_config.py @@ -0,0 +1,83 @@ +# Copyright (C) 2026 Robotec.AI +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""from_config dispatches on config strings, so the values TRANSCRIBE_MODELS +advertises and the configurator writes have to be the ones it matches on.""" + +from pathlib import Path + +import pytest + +from rai_s2s.asr import models +from rai_s2s.asr.agents.asr_agent import SpeechRecognitionAgent +from rai_s2s.asr.agents.initialization import TRANSCRIBE_MODELS + +CONFIG = """ +[asr] +recording_device_name = "default" +transcription_model = "{transcription_model}" +transcription_model_name = "tiny" +language = "en" +vad_model = "{vad_model}" +vad_threshold = 0.3 +silence_grace_period = 0.3 +use_wake_word = false +wake_word_model = "" +wake_word_model_name = "" +wake_word_threshold = 0.5 +""" + + +@pytest.fixture +def config(tmp_path: Path): + def write(transcription_model: str, vad_model: str = "SileroVAD") -> str: + path = tmp_path / "config.toml" + path.write_text( + CONFIG.format(transcription_model=transcription_model, vad_model=vad_model) + ) + return str(path) + + return write + + +@pytest.fixture(autouse=True) +def stub_models(monkeypatch): + """The dispatch is under test, not the models it reaches for.""" + for name in ("LocalWhisper", "FasterWhisper", "OpenAIWhisper", "SileroVAD"): + monkeypatch.setattr(models, name, lambda *args, **kwargs: object()) + monkeypatch.setattr( + SpeechRecognitionAgent, "__init__", lambda self, *args, **kwargs: None + ) + + +@pytest.mark.parametrize("transcription_model", TRANSCRIBE_MODELS) +def test_every_advertised_model_is_dispatchable(config, transcription_model): + assert isinstance( + SpeechRecognitionAgent.from_config(config(transcription_model)), + SpeechRecognitionAgent, + ) + + +def test_unknown_transcription_model_lists_the_valid_ones(config): + # the old suffixed spelling is now rejected at config load, not silently + # accepted and then dropped by the dispatch below + with pytest.raises(ValueError, match="unknown model_type"): + SpeechRecognitionAgent.from_config(config("LocalWhisper (Free)")) + + +def test_unknown_vad_model_is_reported(config): + with pytest.raises(ValueError, match="Unknown VAD model"): + SpeechRecognitionAgent.from_config( + config(TRANSCRIBE_MODELS[0], vad_model="NotAVAD") + ) diff --git a/uv.lock b/uv.lock index 5361650cc..3c0ed6bb8 100644 --- a/uv.lock +++ b/uv.lock @@ -4890,7 +4890,7 @@ requires-dist = [ [[package]] name = "rai-s2s" -version = "1.0.1" +version = "1.0.2" source = { editable = "src/rai_s2s" } dependencies = [ { name = "onnxruntime", version = "1.23.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11' and platform_machine == 'x86_64'" },