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:
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user