Files
local-transcriber/tests/test_backend_openvino.py
T
ddadminandClaude Opus 4.6 42bfbe5280 feat(openvino): реализован OpenVINO бэкенд транскрипции
- Зачем:
  - ускорение транскрипции на x86 CPU (Intel/AMD) в 2-4 раза через OpenVINO GenAI.
- Что:
  - создан backends/openvino.py: OpenVINOBackend с ensure_model_available, create_model, transcribe.
  - модели скачиваются из HuggingFace (OpenVINO/whisper-*-ov), формат OpenVINO IR.
  - аудио декодируется через faster_whisper.decode_audio (PyAV) → .tolist() → pipe.generate(return_timestamps=True).
  - контракт compute_type: явный --compute-type уважается; из дефолтов large-v3 получает fp16 автоматически.
  - openvino-genai добавлен в pyproject.toml с platform markers (x86_64/AMD64, не macOS).
  - compute_type_explicit прокинут через get_backend → load_model → CLI.
  - 16 тестов для OpenVINO бэкенда: resolve_repo, ensure, create, transcribe, validate.
- Проверка:
  - uv run pytest -v — 119 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 23:28:22 +03:00

234 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Тесты для OpenVINO бэкенда."""
from pathlib import Path
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
from local_transcriber.backends.openvino import (
MODEL_REPOS,
OpenVINOBackend,
_validate_model_dir,
)
from local_transcriber.types import Segment
# === _resolve_repo ===
def test_resolve_repo_exact_match():
backend = OpenVINOBackend(compute_type_explicit=True)
assert backend._resolve_repo("medium", "int8") == "OpenVINO/whisper-medium-int8-ov"
def test_resolve_repo_large_v3_fp16():
backend = OpenVINOBackend(compute_type_explicit=True)
assert backend._resolve_repo("large-v3", "fp16") == "OpenVINO/whisper-large-v3-fp16-ov"
def test_resolve_repo_explicit_unsupported_pair_raises():
"""Явный --compute-type с несуществующей парой → ошибка."""
backend = OpenVINOBackend(compute_type_explicit=True)
with pytest.raises(ValueError, match="недоступна с compute_type='fp16'"):
backend._resolve_repo("medium", "fp16")
def test_resolve_repo_explicit_unknown_model_raises():
backend = OpenVINOBackend(compute_type_explicit=True)
with pytest.raises(ValueError, match="не найдена для OpenVINO"):
backend._resolve_repo("distil-large-v3", "int8")
def test_resolve_repo_implicit_fallback():
"""Неявный compute_type: если int8 недоступен для base, fallback на fp16."""
backend = OpenVINOBackend(compute_type_explicit=False)
# base + int8 не существует, но base + fp16 есть
assert backend._resolve_repo("base", "int8") == "OpenVINO/whisper-base-fp16-ov"
def test_resolve_repo_implicit_large_v3_prefers_fp16():
"""Неявный compute_type: large-v3 автоматически получает fp16."""
backend = OpenVINOBackend(compute_type_explicit=False)
# Дефолт int8, но для large-v3 override на fp16
assert backend._resolve_repo("large-v3", "int8") == "OpenVINO/whisper-large-v3-fp16-ov"
def test_resolve_repo_explicit_large_v3_int8_respected():
"""Явный --compute-type int8 для large-v3 → уважается."""
backend = OpenVINOBackend(compute_type_explicit=True)
assert backend._resolve_repo("large-v3", "int8") == "OpenVINO/whisper-large-v3-int8-ov"
# === ensure_model_available ===
@patch("local_transcriber.backends.openvino.snapshot_download")
def test_ensure_model_available_cache_hit(mock_download, tmp_path):
model_dir = tmp_path / "model"
model_dir.mkdir()
(model_dir / "openvino_encoder_model.xml").write_text("<xml/>")
(model_dir / "openvino_decoder_model.xml").write_text("<xml/>")
mock_download.return_value = str(model_dir)
backend = OpenVINOBackend(compute_type_explicit=True)
result = backend.ensure_model_available("medium", "int8")
assert result == str(model_dir)
mock_download.assert_called_once()
assert mock_download.call_args.kwargs["local_files_only"] is True
@patch("local_transcriber.backends.openvino.snapshot_download")
def test_ensure_model_available_downloads(mock_download, tmp_path):
from huggingface_hub.errors import LocalEntryNotFoundError
model_dir = tmp_path / "downloaded"
model_dir.mkdir()
(model_dir / "openvino_encoder_model.xml").write_text("<xml/>")
(model_dir / "openvino_decoder_model.xml").write_text("<xml/>")
mock_download.side_effect = [
LocalEntryNotFoundError("not cached"),
str(model_dir),
]
backend = OpenVINOBackend(compute_type_explicit=True)
statuses: list[str] = []
result = backend.ensure_model_available("medium", "int8", on_status=statuses.append)
assert result == str(model_dir)
assert any("Скачиваю" in s for s in statuses)
# === create_model ===
def test_create_model():
mock_ov = MagicMock()
mock_pipeline = MagicMock()
mock_ov.WhisperPipeline.return_value = mock_pipeline
backend = OpenVINOBackend()
with patch.dict("sys.modules", {"openvino_genai": mock_ov}):
model = backend.create_model("/path/to/model", "openvino", "int8")
mock_ov.WhisperPipeline.assert_called_once_with("/path/to/model", "CPU")
assert model is mock_pipeline
# === transcribe ===
def test_transcribe_maps_chunks_to_segments():
"""Проверяет маппинг chunks → Segment[] и формат языка."""
backend = OpenVINOBackend()
mock_model = MagicMock()
chunk1 = MagicMock()
chunk1.start_ts = 0.0
chunk1.end_ts = 3.5
chunk1.text = " Привет мир"
chunk2 = MagicMock()
chunk2.start_ts = 3.5
chunk2.end_ts = 7.0
chunk2.text = " Тестовый сегмент"
mock_result = MagicMock()
mock_result.chunks = [chunk1, chunk2]
mock_model.generate.return_value = mock_result
raw_audio = np.zeros(16000 * 10, dtype=np.float32) # 10 секунд
with patch("faster_whisper.decode_audio", return_value=raw_audio):
result = backend.transcribe(
mock_model, Path("test.mp3"), language="ru",
)
assert len(result.segments) == 2
assert result.segments[0].text == " Привет мир"
assert result.segments[0].start == 0.0
assert result.segments[0].end == 3.5
assert result.duration == 10.0
# Проверяем формат языка для OpenVINO GenAI
call_kwargs = mock_model.generate.call_args
assert call_kwargs.kwargs["language"] == "<|ru|>"
assert call_kwargs.kwargs["return_timestamps"] is True
def test_transcribe_calls_tolist():
"""raw_speech передаётся как list, не ndarray."""
backend = OpenVINOBackend()
mock_model = MagicMock()
mock_result = MagicMock()
mock_result.chunks = []
mock_model.generate.return_value = mock_result
raw_audio = np.zeros(160, dtype=np.float32)
with patch("faster_whisper.decode_audio", return_value=raw_audio):
backend.transcribe(mock_model, Path("test.mp3"), language=None)
call_args = mock_model.generate.call_args[0][0]
assert isinstance(call_args, list)
def test_transcribe_no_language_auto():
"""Без указания языка — не передаём language в generate."""
backend = OpenVINOBackend()
mock_model = MagicMock()
mock_result = MagicMock()
mock_result.chunks = []
mock_model.generate.return_value = mock_result
raw_audio = np.zeros(160, dtype=np.float32)
with patch("faster_whisper.decode_audio", return_value=raw_audio):
result = backend.transcribe(mock_model, Path("test.mp3"), language=None)
call_kwargs = mock_model.generate.call_args.kwargs
assert "language" not in call_kwargs
assert result.language == "auto"
assert result.language_probability == 0.0
def test_transcribe_calls_on_segment():
backend = OpenVINOBackend()
mock_model = MagicMock()
chunk = MagicMock()
chunk.start_ts = 0.0
chunk.end_ts = 2.0
chunk.text = " Test"
mock_result = MagicMock()
mock_result.chunks = [chunk]
mock_model.generate.return_value = mock_result
raw_audio = np.zeros(16000, dtype=np.float32)
callback = MagicMock()
with patch("faster_whisper.decode_audio", return_value=raw_audio):
backend.transcribe(
mock_model, Path("test.mp3"), language="en", on_segment=callback,
)
callback.assert_called_once()
seg = callback.call_args[0][0]
assert isinstance(seg, Segment)
assert seg.text == " Test"
# === _validate_model_dir ===
def test_validate_model_dir_ok(tmp_path):
(tmp_path / "openvino_encoder_model.xml").write_text("<xml/>")
(tmp_path / "openvino_decoder_model.xml").write_text("<xml/>")
_validate_model_dir(tmp_path) # should not raise
def test_validate_model_dir_missing(tmp_path):
(tmp_path / "openvino_encoder_model.xml").write_text("<xml/>")
with pytest.raises(ValueError, match="openvino_decoder_model.xml"):
_validate_model_dir(tmp_path)