diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 3b76bdb..eb48942 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -55,7 +55,7 @@ def main( ), compute_type: str | None = typer.Option( 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="Подробный вывод"), force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"), @@ -72,7 +72,7 @@ def main( resolved_device = detect_device(defaults["device"]) 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) if not expanded: diff --git a/src/local_transcriber/transcriber.py b/src/local_transcriber/transcriber.py index 1561707..6bf5871 100644 --- a/src/local_transcriber/transcriber.py +++ b/src/local_transcriber/transcriber.py @@ -118,6 +118,7 @@ def transcribe( """High-level API: загрузка модели + транскрипция за один вызов.""" model, actual_device, backend, model_path = load_model( model_name, device, compute_type, on_status, strict_device, + compute_type_explicit=True, # Python API — caller explicitly chose compute_type ) tfr = _transcribe_file( model, actual_device, backend, model_path, @@ -130,11 +131,19 @@ def transcribe( def ensure_model_available( model_name: str, device: str = "cpu", - compute_type: str = "float32", + compute_type: str | None = None, on_status: Callable[[str], None] | None = None, ) -> str: """Публичный 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) @@ -154,9 +163,14 @@ def _is_backend_error(exc: BaseException, device: str) -> bool: def _is_openvino_error(exc: BaseException) -> bool: - """Проверка ошибок OpenVINO runtime.""" - msg = str(exc).lower() - return any(k in msg for k in ("openvino", "ov_", "inference_engine")) + """Проверка ошибок OpenVINO runtime. + + 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: diff --git a/tests/test_transcriber.py b/tests/test_transcriber.py index ecfb96d..19d2713 100644 --- a/tests/test_transcriber.py +++ b/tests/test_transcriber.py @@ -512,3 +512,32 @@ def test_openvino_strict_device_no_fallback(mock_get_backend): with pytest.raises(RuntimeError, match="OpenVINO"): 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"