275 lines
6.1 KiB
Python
275 lines
6.1 KiB
Python
"""
|
|
Typed configuration models for the Voice Assistant.
|
|
|
|
Purpose:
|
|
Define and validate the configuration structures used by the
|
|
application and dynamic provider loading system.
|
|
|
|
Responsibilities:
|
|
- Define the application configuration for STT, Agent, and TTS.
|
|
- Define the provider catalog structure.
|
|
- Represent provider-specific options without coupling the core
|
|
configuration model to a specific vendor or implementation.
|
|
- Describe provider capabilities for discovery and future tooling.
|
|
|
|
Non-responsibilities:
|
|
- Loading YAML files.
|
|
- Reading environment variables or secrets.
|
|
- Selecting or instantiating provider implementations.
|
|
- Executing STT, Agent, or TTS functionality.
|
|
|
|
Architecture:
|
|
YAML
|
|
|
|
|
v
|
|
Config Loader
|
|
|
|
|
v
|
|
AppConfig / ProviderCatalog
|
|
|
|
|
v
|
|
Provider Loader
|
|
|
|
|
v
|
|
STTAdapter / AgentAdapter / TTSAdapter
|
|
"""
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
from voice_assistant.config.loader import (
|
|
load_config,
|
|
load_provider_catalog,
|
|
)
|
|
|
|
|
|
def test_load_development_config():
|
|
config = load_config()
|
|
|
|
assert config.stt.provider == "nvidia"
|
|
assert config.stt.options["server"] == "127.0.0.1:9000"
|
|
assert config.stt.options["language_code"] == "pt-BR"
|
|
assert config.stt.options["automatic_punctuation"] is True
|
|
|
|
assert config.tts.provider == "voxcpm"
|
|
assert config.tts.options["server"] == "http://127.0.0.1:8000"
|
|
assert config.tts.options["model"] == "openbmb/VoxCPM2"
|
|
assert config.tts.options["voice"] == "default"
|
|
|
|
assert config.agent.provider == "mock"
|
|
|
|
|
|
def test_load_config_missing_file(tmp_path):
|
|
with pytest.raises(
|
|
FileNotFoundError,
|
|
match="Configuration file not found",
|
|
):
|
|
load_config(
|
|
environment="development",
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
|
|
def test_load_config_empty_file(tmp_path):
|
|
config_file = tmp_path / "development.yaml"
|
|
config_file.write_text(
|
|
"",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Configuration file is empty",
|
|
):
|
|
load_config(
|
|
environment="development",
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
|
|
def test_load_config_root_must_be_mapping(tmp_path):
|
|
config_file = tmp_path / "development.yaml"
|
|
config_file.write_text(
|
|
"- item1\n- item2\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Configuration root must be a mapping",
|
|
):
|
|
load_config(
|
|
environment="development",
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
|
|
def test_load_config_validates_required_fields(tmp_path):
|
|
config_file = tmp_path / "development.yaml"
|
|
|
|
config_file.write_text(
|
|
yaml.safe_dump(
|
|
{
|
|
"stt": {
|
|
"provider": "nvidia",
|
|
"server": "127.0.0.1:9000",
|
|
"language": "pt-BR",
|
|
},
|
|
"tts": {
|
|
"provider": "voxcpm",
|
|
"server": "http://127.0.0.1:8000",
|
|
"model": "openbmb/VoxCPM2",
|
|
"voice": "default",
|
|
},
|
|
# agent intentionally missing
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
with pytest.raises(
|
|
Exception,
|
|
match="agent",
|
|
):
|
|
load_config(
|
|
environment="development",
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
def test_load_provider_catalog():
|
|
catalog = load_provider_catalog()
|
|
|
|
assert "nvidia" in catalog.providers["stt"]
|
|
assert "voxcpm" in catalog.providers["tts"]
|
|
assert "mock" in catalog.providers["agent"]
|
|
|
|
voxcpm = catalog.providers["tts"]["voxcpm"]
|
|
|
|
assert voxcpm.name == "VoxCPM"
|
|
assert (
|
|
voxcpm.implementation
|
|
== "voice_assistant.tts.voxcpm:VoxCPMTTSAdapter"
|
|
)
|
|
assert voxcpm.capabilities == {
|
|
"realtime": True,
|
|
"streaming": True,
|
|
}
|
|
|
|
|
|
def test_load_provider_catalog_missing_file(tmp_path):
|
|
with pytest.raises(
|
|
FileNotFoundError,
|
|
match="Provider catalog not found",
|
|
):
|
|
load_provider_catalog(
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
|
|
def test_load_config_uses_development_app_env(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
development = tmp_path / "development.yaml"
|
|
|
|
development.write_text(
|
|
"""
|
|
stt:
|
|
provider: nvidia
|
|
options:
|
|
server: "dev-asr"
|
|
|
|
tts:
|
|
provider: voxcpm
|
|
options:
|
|
server: "dev-tts"
|
|
model: "dev-model"
|
|
voice: "default"
|
|
|
|
agent:
|
|
provider: mock
|
|
options: {}
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
monkeypatch.setenv(
|
|
"APP_ENV",
|
|
"development",
|
|
)
|
|
|
|
config = load_config(
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
assert config.stt.options["server"] == "dev-asr"
|
|
assert config.tts.options["server"] == "dev-tts"
|
|
assert config.tts.options["model"] == "dev-model"
|
|
|
|
|
|
def test_load_config_rejects_unknown_environment(
|
|
tmp_path,
|
|
):
|
|
with pytest.raises(
|
|
FileNotFoundError,
|
|
match="Configuration file not found",
|
|
):
|
|
load_config(
|
|
environment="does-not-exist",
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
def test_load_config_uses_staging_app_env(
|
|
tmp_path,
|
|
monkeypatch,
|
|
):
|
|
config_file = tmp_path / "staging.yaml"
|
|
|
|
config_file.write_text(
|
|
"""
|
|
stt:
|
|
provider: nvidia
|
|
options:
|
|
server: "staging-asr:9000"
|
|
|
|
tts:
|
|
provider: voxcpm
|
|
options:
|
|
server: "http://staging-tts:8000"
|
|
model: "staging-model"
|
|
voice: "staging-voice"
|
|
|
|
agent:
|
|
provider: mock
|
|
options: {}
|
|
""",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
monkeypatch.setenv(
|
|
"APP_ENV",
|
|
"staging",
|
|
)
|
|
|
|
config = load_config(
|
|
config_dir=tmp_path,
|
|
)
|
|
|
|
assert config.stt.provider == "nvidia"
|
|
assert (
|
|
config.stt.options["server"]
|
|
== "staging-asr:9000"
|
|
)
|
|
|
|
assert config.tts.provider == "voxcpm"
|
|
assert (
|
|
config.tts.options["server"]
|
|
== "http://staging-tts:8000"
|
|
)
|
|
|
|
assert config.tts.options["model"] == "staging-model"
|
|
assert config.tts.options["voice"] == "staging-voice"
|
|
|
|
assert config.agent.provider == "mock"
|
|
|