fix(openvino): исправлены баги compute_type, fallback и help text

- Зачем:
  - compute_type из конфига терялся как implicit, ломая выбор модели OpenVINO.
  - узкий матчинг ошибок пропускал реальные OpenVINO runtime failures.
  - ensure_model_available(device="openvino") падал из-за дефолта float32.
  - ensure_model_available не передавал compute_type_explicit, ломая large-v3 → fp16 override.
  - help по --compute-type не отражал дефолт для OpenVINO.
- Что:
  - ct_explicit учитывает и CLI-флаг, и конфиг-файл.
  - публичный API transcribe() передаёт compute_type_explicit=True.
  - _is_openvino_error() ловит любой RuntimeError.
  - ensure_model_available: дефолт compute_type из DEVICE_DEFAULTS, explicit прокинут в get_backend.
  - --compute-type help обновлён: float16 (CUDA) / int8 (OpenVINO) / float32 (CPU).
- Проверка:
  - uv run pytest -v — 124 passed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 23:45:27 +03:00
co-authored by Claude Opus 4.6
parent ee9e017083
commit 262bfd91d1
3 changed files with 50 additions and 7 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ def main(
), ),
compute_type: str | None = typer.Option( compute_type: str | None = typer.Option(
None, "--compute-type", show_default=False, None, "--compute-type", show_default=False,
help="Тип вычислений [по умолч.: float16 (GPU) / float32 (CPU)]" help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO) / float32 (CPU)]"
), ),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"), verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"), force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"),
@@ -72,7 +72,7 @@ def main(
resolved_device = detect_device(defaults["device"]) resolved_device = detect_device(defaults["device"])
defaults = apply_device_defaults(defaults, resolved_device, cli_values, config) defaults = apply_device_defaults(defaults, resolved_device, cli_values, config)
ct_explicit = compute_type is not None ct_explicit = compute_type is not None or "compute_type" in config
expanded = expand_globs(files) expanded = expand_globs(files)
if not expanded: if not expanded:
+19 -5
View File
@@ -118,6 +118,7 @@ def transcribe(
"""High-level API: загрузка модели + транскрипция за один вызов.""" """High-level API: загрузка модели + транскрипция за один вызов."""
model, actual_device, backend, model_path = load_model( model, actual_device, backend, model_path = load_model(
model_name, device, compute_type, on_status, strict_device, model_name, device, compute_type, on_status, strict_device,
compute_type_explicit=True, # Python API — caller explicitly chose compute_type
) )
tfr = _transcribe_file( tfr = _transcribe_file(
model, actual_device, backend, model_path, model, actual_device, backend, model_path,
@@ -130,11 +131,19 @@ def transcribe(
def ensure_model_available( def ensure_model_available(
model_name: str, model_name: str,
device: str = "cpu", device: str = "cpu",
compute_type: str = "float32", compute_type: str | None = None,
on_status: Callable[[str], None] | None = None, on_status: Callable[[str], None] | None = None,
) -> str: ) -> str:
"""Публичный helper: гарантирует наличие модели для указанного бэкенда.""" """Публичный helper: гарантирует наличие модели для указанного бэкенда."""
backend = get_backend(device) from local_transcriber.config import DEVICE_DEFAULTS, HARDCODED_DEFAULTS
if compute_type is None:
device_defs = DEVICE_DEFAULTS.get(device, {})
compute_type = device_defs.get("compute_type", HARDCODED_DEFAULTS["compute_type"])
explicit = False
else:
explicit = True
backend = get_backend(device, compute_type_explicit=explicit)
return backend.ensure_model_available(model_name, compute_type, on_status) return backend.ensure_model_available(model_name, compute_type, on_status)
@@ -154,9 +163,14 @@ def _is_backend_error(exc: BaseException, device: str) -> bool:
def _is_openvino_error(exc: BaseException) -> bool: def _is_openvino_error(exc: BaseException) -> bool:
"""Проверка ошибок OpenVINO runtime.""" """Проверка ошибок OpenVINO runtime.
msg = str(exc).lower()
return any(k in msg for k in ("openvino", "ov_", "inference_engine")) OpenVINO runtime кидает RuntimeError с разнообразными сообщениями
(openvino, ov_, inference, plugins, src/...). Пользовательские ошибки
(файл не найден, неверный формат) приходят как FileNotFoundError/ValueError
и не попадают сюда. Поэтому для RuntimeError считаем это backend failure.
"""
return isinstance(exc, RuntimeError)
def _notify_status(on_status: Callable[[str], None] | None, message: str) -> None: def _notify_status(on_status: Callable[[str], None] | None, message: str) -> None:
+29
View File
@@ -512,3 +512,32 @@ def test_openvino_strict_device_no_fallback(mock_get_backend):
with pytest.raises(RuntimeError, match="OpenVINO"): with pytest.raises(RuntimeError, match="OpenVINO"):
load_model("medium", "openvino", "int8", strict_device=True) load_model("medium", "openvino", "int8", strict_device=True)
@patch("local_transcriber.transcriber.get_backend")
def test_openvino_runtime_error_triggers_fallback(mock_get_backend):
"""Любой RuntimeError от OpenVINO бэкенда → fallback."""
ov_backend = _make_backend(
create_model_error=RuntimeError("Exception from src/inference/..."),
)
cpu_backend = _make_backend(model_path="/mock/cpu/model")
def backend_for_device(device, **kwargs):
return ov_backend if device == "openvino" else cpu_backend
mock_get_backend.side_effect = backend_for_device
with pytest.warns(UserWarning, match="Переключение на CPU"):
_, actual_device, _, _ = load_model("medium", "openvino", "int8")
assert actual_device == "cpu"
def test_ensure_model_available_openvino_default_compute_type():
"""ensure_model_available(device='openvino') без compute_type не падает."""
from local_transcriber.backends.openvino import OpenVINOBackend
backend = OpenVINOBackend(compute_type_explicit=True)
# Проверяем что _resolve_repo работает с дефолтным compute_type для openvino (int8)
repo = backend._resolve_repo("medium", "int8")
assert repo == "OpenVINO/whisper-medium-int8-ov"