first commit
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

This commit is contained in:
2026-07-23 02:38:05 +03:00
commit 8757bec8ac
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
"""Тесты для загрузчика конфига плагинов и factory."""
import sys
import types
from dataclasses import dataclass
from pathlib import Path
from typing import Any
import pytest
from core.plugins.config import PluginsConfig, TranscriberConfig, load_plugins_config
from core.plugins.factory import UnknownProviderError, create_summarizer, create_transcriber
from core.plugins.faster_whisper import FasterWhisperCPU, FasterWhisperGPU
from core.plugins.null import NullSummarizer
PLUGINS_YAML = Path(__file__).parent.parent.parent / "config" / "plugins.yaml"
def test_load_plugins_config_from_yaml() -> None:
config = load_plugins_config(PLUGINS_YAML)
assert isinstance(config, PluginsConfig)
assert config.transcriber.enabled is True
assert config.transcriber.provider == "faster_whisper_cpu"
assert config.transcriber.model == "small"
assert config.transcriber.language == "ru"
assert config.summarizer.provider == "null"
assert config.summarizer.chunk_minutes == 20
assert config.chat.enabled is True
def test_factory_instantiates_faster_whisper_transcriber_from_config() -> None:
config = load_plugins_config(PLUGINS_YAML)
transcriber = create_transcriber(config.transcriber)
assert isinstance(transcriber, FasterWhisperCPU)
assert transcriber.model_name == "small"
assert transcriber.language == "ru"
# Путь — КОНТРАКТ с backend/services/ai_tiers.py (WHISPER_MODELS_ROOT,
# ADR-004): модель уровня хранится в подкаталоге по имени модели,
# не в плоском корне тома (см. комментарий в config/plugins.yaml).
assert transcriber.download_root == "/models/whisper/small"
def test_factory_instantiates_null_summarizer() -> None:
config = load_plugins_config(PLUGINS_YAML)
summarizer = create_summarizer(config.summarizer)
assert isinstance(summarizer, NullSummarizer)
assert summarizer.summarize("some transcript") == ""
def test_factory_unknown_transcriber_provider_raises() -> None:
config = load_plugins_config(PLUGINS_YAML)
config.transcriber.provider = "does-not-exist"
with pytest.raises(UnknownProviderError):
create_transcriber(config.transcriber)
def test_factory_unknown_summarizer_provider_raises() -> None:
config = load_plugins_config(PLUGINS_YAML)
config.summarizer.provider = "does-not-exist"
with pytest.raises(UnknownProviderError):
create_summarizer(config.summarizer)
def test_factory_creates_faster_whisper_without_importing_it(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`create_transcriber` для `faster_whisper_cpu` не импортирует пакет `faster_whisper`.
Ленивость: тяжёлая зависимость грузится только внутри `transcribe()`, а не
при инстанцировании плагина фабрикой (важно для API-процесса).
"""
monkeypatch.delitem(sys.modules, "faster_whisper", raising=False)
config = load_plugins_config(PLUGINS_YAML)
transcriber = create_transcriber(config.transcriber)
assert isinstance(transcriber, FasterWhisperCPU)
assert "faster_whisper" not in sys.modules
@dataclass
class _FakeRawSegment:
"""Сегмент, возвращаемый заглушкой `WhisperModel.transcribe`."""
start: float
end: float
text: str
class _FakeWhisperModel:
"""Заглушка `faster_whisper.WhisperModel`, фиксирующая переданные аргументы."""
last_init_kwargs: dict[str, Any] | None = None
last_transcribe_kwargs: dict[str, Any] | None = None
def __init__(self, model_size_or_path: str, **kwargs: Any) -> None:
self.model_size_or_path = model_size_or_path
_FakeWhisperModel.last_init_kwargs = kwargs
def transcribe(self, audio_path: str, **kwargs: Any) -> tuple[list[_FakeRawSegment], object]:
_FakeWhisperModel.last_transcribe_kwargs = kwargs
segments = [
_FakeRawSegment(0.0, 2.0, "длинный сегмент"),
_FakeRawSegment(2.0, 2.2, "коротыш"), # 0.2с < 0.3с — отбрасывается
_FakeRawSegment(2.2, 2.6, "ровно на границе"), # 0.4с — остаётся
]
return segments, object()
def test_faster_whisper_transcribe_forwards_params_and_filters_short_segments(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Юнит `FasterWhisperCPU` с замоканным `WhisperModel`: прокидывание
language/vad_filter/vad_parameters и отбрасывание сегментов короче 0.3с.
"""
fake_module = types.ModuleType("faster_whisper")
fake_module.WhisperModel = _FakeWhisperModel # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "faster_whisper", fake_module)
monkeypatch.setattr(FasterWhisperCPU, "_model", None)
transcriber = FasterWhisperCPU(model="small", language="ru", download_root="/models/whisper")
result = transcriber.transcribe("audio.ogg", language="ru")
assert [segment.text for segment in result] == ["длинный сегмент", "ровно на границе"]
assert _FakeWhisperModel.last_init_kwargs == {
"device": "cpu",
"compute_type": "int8",
"download_root": "/models/whisper",
}
assert _FakeWhisperModel.last_transcribe_kwargs == {
"language": "ru",
"vad_filter": True,
"vad_parameters": {"min_silence_duration_ms": 500},
}
def test_factory_instantiates_faster_whisper_gpu_transcriber() -> None:
"""`create_transcriber` для `faster_whisper_gpu` (уровни `medium`/`max`, ADR-004)."""
config = TranscriberConfig(
provider="faster_whisper_gpu",
model="large-v3",
options={"download_root": "/models/whisper/large-v3", "compute_type": "float16"},
)
transcriber = create_transcriber(config)
assert isinstance(transcriber, FasterWhisperGPU)
assert transcriber.model_name == "large-v3"
assert transcriber.download_root == "/models/whisper/large-v3"
assert transcriber.compute_type == "float16"
assert transcriber.device == "cuda"
def test_faster_whisper_gpu_transcribe_forwards_cuda_device_and_compute_type(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""GPU-плагин прокидывает `device="cuda"`/`compute_type` в `WhisperModel`."""
fake_module = types.ModuleType("faster_whisper")
fake_module.WhisperModel = _FakeWhisperModel # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "faster_whisper", fake_module)
monkeypatch.setattr(FasterWhisperGPU, "_model", None)
transcriber = FasterWhisperGPU(model="large-v3", download_root="/models/whisper/large-v3")
result = transcriber.transcribe("audio.ogg", language="ru")
assert [segment.text for segment in result] == ["длинный сегмент", "ровно на границе"]
assert _FakeWhisperModel.last_init_kwargs == {
"device": "cuda",
"compute_type": "float16",
"download_root": "/models/whisper/large-v3",
}
def test_cpu_and_gpu_transcribers_have_independent_model_singletons(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""CPU- и GPU-плагины не делят синглтон `_model` — у каждого подкласса свой атрибут."""
monkeypatch.setattr(FasterWhisperCPU, "_model", "cpu-sentinel")
monkeypatch.setattr(FasterWhisperGPU, "_model", None)
assert FasterWhisperCPU._model == "cpu-sentinel"
assert FasterWhisperGPU._model is None