fix(openvino): корректный compute_type в CLI + индикатор прогресса

- Зачем:
  - CLI показывал int8, хотя реально использовался fp16 для large-v3.
  - при транскрипции OpenVINO не было индикации прогресса.
- Что:
  - _resolve_repo возвращает (repo_id, actual_compute_type).
  - бэкенды сохраняют actual_compute_type после ensure_model_available.
  - CLI выводит фактический compute_type после load_model, а не дефолтный.
  - generate() запускается в потоке, статус обновляется каждую секунду с elapsed time.
- Проверка:
  - uv run pytest -q — 124 passed.
  - uv run transcribe file.mp4 --device openvino --model large-v3 показывает fp16.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 11:58:29 +03:00
co-authored by Claude Opus 4.6
parent f5345031ef
commit f42d3ae842
5 changed files with 65 additions and 18 deletions
+48 -7
View File
@@ -2,6 +2,8 @@
from __future__ import annotations
import threading
import time
import warnings
from collections.abc import Callable
from pathlib import Path
@@ -47,6 +49,7 @@ class OpenVINOBackend:
def __init__(self, compute_type_explicit: bool = True):
"""compute_type_explicit=False означает, что compute_type пришёл из дефолтов."""
self._compute_type_explicit = compute_type_explicit
self.actual_compute_type: str | None = None
def ensure_model_available(
self,
@@ -55,7 +58,8 @@ class OpenVINOBackend:
on_status: Callable[[str], None] | None = None,
) -> str:
"""Скачивает/находит OpenVINO модель нужной квантизации."""
repo_id = self._resolve_repo(model_name, compute_type)
repo_id, resolved_ct = self._resolve_repo(model_name, compute_type)
self.actual_compute_type = resolved_ct
try:
_notify(on_status, f"Проверяю кэш модели {model_name} (OpenVINO)...")
@@ -102,8 +106,9 @@ class OpenVINOBackend:
if language:
kwargs["language"] = f"<|{language}|>"
_notify(on_status, "Транскрибирую (OpenVINO)...")
result = model.generate(raw_speech.tolist(), **kwargs)
duration_str = f"{int(duration // 60):02d}:{int(duration % 60):02d}"
pcm_list = raw_speech.tolist()
result = _generate_with_progress(model, pcm_list, kwargs, duration_str, on_status)
segments: list[Segment] = []
if hasattr(result, "chunks") and result.chunks:
@@ -134,8 +139,11 @@ class OpenVINOBackend:
device_used="", # оркестратор проставит
)
def _resolve_repo(self, model_name: str, compute_type: str) -> str:
"""Находит HF repo для пары (model, compute_type) с fallback."""
def _resolve_repo(self, model_name: str, compute_type: str) -> tuple[str, str]:
"""Находит HF repo для пары (model, compute_type) с fallback.
Возвращает (repo_id, actual_compute_type).
"""
# Для неявного compute_type: override для конкретных моделей
if not self._compute_type_explicit and model_name in _IMPLICIT_COMPUTE_TYPE_OVERRIDES:
compute_type = _IMPLICIT_COMPUTE_TYPE_OVERRIDES[model_name]
@@ -143,7 +151,7 @@ class OpenVINOBackend:
# Точное совпадение
repo = MODEL_REPOS.get((model_name, compute_type))
if repo:
return repo
return repo, compute_type
# Fallback только для неявного compute_type
if not self._compute_type_explicit:
@@ -151,7 +159,7 @@ class OpenVINOBackend:
for fallback_ct in fallbacks:
repo = MODEL_REPOS.get((model_name, fallback_ct))
if repo:
return repo
return repo, fallback_ct
# Явный --compute-type с несуществующей парой → ошибка
available = [ct for (m, ct) in MODEL_REPOS if m == model_name]
@@ -168,6 +176,39 @@ class OpenVINOBackend:
)
def _generate_with_progress(
model: Any,
pcm_list: list[float],
kwargs: dict[str, Any],
duration_str: str,
on_status: Callable[[str], None] | None,
) -> Any:
"""Запускает model.generate() в потоке, обновляя статус с elapsed time."""
result_box: list[Any] = [None]
error_box: list[BaseException | None] = [None]
def run() -> None:
try:
result_box[0] = model.generate(pcm_list, **kwargs)
except BaseException as exc:
error_box[0] = exc
thread = threading.Thread(target=run)
start = time.monotonic()
thread.start()
while thread.is_alive():
elapsed = int(time.monotonic() - start)
elapsed_str = f"{elapsed // 60:02d}:{elapsed % 60:02d}"
_notify(on_status, f"Транскрибирую (OpenVINO)... {elapsed_str} / {duration_str} аудио")
thread.join(timeout=1.0)
if error_box[0] is not None:
raise error_box[0]
return result_box[0]
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
if on_status is not None:
on_status(message)