feat(openvino): добавлена поддержка Intel GPU через OpenVINO

- Зачем:
  - OpenVINO backend хардкодил "CPU", хотя Intel Arc GPU доступен и даёт ~2x ускорение.
- Что:
  - новые device modes: --device openvino-gpu, openvino-cpu; openvino — авто-детект GPU/CPU.
  - detect_device() проверяет Intel GPU через OpenVINO Core API (с fail-safe).
  - OpenVINOBackend передаёт "GPU"/"CPU" в WhisperPipeline вместо хардкода "CPU".
  - подсказка "Совет: --model large-v3" при наличии GPU и модели не large-v3.
  - .gitattributes для нормализации line endings (eol=lf).
  - 150 тестов, включая GPU detection, routing, fallback, CLI device info.
  - README, docs/gpu.md, docs/PRD.md обновлены для Intel GPU.
- Проверка:
  - uv run pytest (150 passed).
  - uv run transcribe --device openvino-gpu file.mp4 на Intel Arc 140T GPU.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dmitry Dementiev
2026-03-22 15:45:27 +03:00
co-authored by Claude Opus 4.6
parent dc59d2a620
commit 617181f2dc
17 changed files with 522 additions and 66 deletions
+4 -2
View File
@@ -14,14 +14,16 @@ def get_backend(device: str, *, compute_type_explicit: bool = True) -> Backend:
Импорты ленивые — бэкенд загружается только при запросе.
compute_type_explicit: False если compute_type пришёл из дефолтов (влияет на fallback).
"""
if device == "openvino":
if device in ("openvino", "openvino-gpu", "openvino-cpu"):
try:
from .openvino import OpenVINOBackend
except ImportError:
raise ValueError(
"OpenVINO бэкенд недоступен. Установите: pip install openvino-genai"
) from None
return OpenVINOBackend(compute_type_explicit=compute_type_explicit)
return OpenVINOBackend(
ov_device=device, compute_type_explicit=compute_type_explicit
)
# cuda, cpu и всё остальное → faster-whisper
from .faster_whisper import FasterWhisperBackend
+29 -3
View File
@@ -46,10 +46,18 @@ MODEL_REQUIRED_FILES = [
class OpenVINOBackend:
"""Бэкенд транскрипции через openvino-genai WhisperPipeline."""
def __init__(self, compute_type_explicit: bool = True):
"""compute_type_explicit=False означает, что compute_type пришёл из дефолтов."""
def __init__(
self,
ov_device: str = "openvino-cpu",
compute_type_explicit: bool = True,
):
"""ov_device: "openvino", "openvino-gpu" или "openvino-cpu".
compute_type_explicit=False означает, что compute_type пришёл из дефолтов.
"""
self._ov_device = ov_device
self._compute_type_explicit = compute_type_explicit
self.actual_compute_type: str | None = None
self.actual_ov_device: str | None = None
def ensure_model_available(
self,
@@ -76,6 +84,22 @@ class OpenVINOBackend:
_validate_model_dir(downloaded_path)
return str(downloaded_path)
def _resolve_ov_device(self) -> str:
"""Резолвит device string в OpenVINO device: ``"GPU"`` или ``"CPU"``."""
if self._ov_device == "openvino-gpu":
return "GPU"
if self._ov_device == "openvino-cpu":
return "CPU"
# "openvino" → авто-детект
try:
from openvino import Core
if "GPU" in Core().available_devices:
return "GPU"
except Exception:
pass
return "CPU"
def create_model(
self,
model_path: str,
@@ -85,7 +109,9 @@ class OpenVINOBackend:
"""Создаёт WhisperPipeline."""
import openvino_genai as ov_genai
return ov_genai.WhisperPipeline(model_path, "CPU")
ov_dev = self._resolve_ov_device()
self.actual_ov_device = ov_dev
return ov_genai.WhisperPipeline(model_path, ov_dev)
def transcribe(
self,
+18 -3
View File
@@ -21,6 +21,7 @@ from .utils import (
detect_device,
expand_globs,
get_gpu_name,
get_intel_gpu_name,
has_existing_transcript,
validate_input_file,
)
@@ -34,7 +35,10 @@ def _format_device_info(device_used: str) -> str:
if device_used == "cuda":
gpu_name = get_gpu_name()
return f"CUDA ({gpu_name or 'Unknown GPU'})"
if device_used == "openvino":
if device_used == "openvino-gpu":
gpu_name = get_intel_gpu_name()
return f"OpenVINO ({gpu_name or 'Intel GPU'})"
if device_used in ("openvino", "openvino-cpu"):
return "OpenVINO (CPU)"
return "CPU"
@@ -51,11 +55,11 @@ def main(
output: Path | None = typer.Option(None, "--output", "-o", help="Путь к выходному файлу"),
device: str | None = typer.Option(
None, "--device", "-d", show_default=False,
help="Устройство (auto|cpu|cuda|openvino) [по умолч.: auto]"
help="Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) [по умолч.: auto]"
),
compute_type: str | None = typer.Option(
None, "--compute-type", show_default=False,
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO) / float32 (CPU)]"
help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]"
),
verbose: bool = typer.Option(False, "--verbose", "-v", help="Подробный вывод"),
force: bool = typer.Option(False, "--force", "-f", help="Перезаписать существующие транскрипты"),
@@ -151,6 +155,11 @@ def _run_single(
f"Устройство: [bold]{actual_device}[/bold] "
f"Compute: [bold]{actual_ct}[/bold]"
)
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3":
console.print(
"Совет: --model large-v3 даёт лучшее качество на GPU (~2x дольше)",
style="dim",
)
with Status("Подготавливаю запуск...", console=console) as status:
tfr = _transcribe_file(
@@ -249,6 +258,12 @@ def _run_batch(
compute_type_explicit=compute_type_explicit,
)
if actual_device == "openvino-gpu" and defaults["model"] != "large-v3":
console.print(
"Совет: --model large-v3 даёт лучшее качество на GPU (~2x дольше)",
style="dim",
)
if actual_device != resolved_device:
if requested_device == "auto":
console.print(
+3 -1
View File
@@ -20,11 +20,13 @@ DEVICE_DEFAULTS: dict[str, dict[str, str]] = {
"cuda": {"model": "medium", "compute_type": "float16"},
"cpu": {"model": "medium", "compute_type": "float32"},
"openvino": {"model": "medium", "compute_type": "int8"},
"openvino-gpu": {"model": "medium", "compute_type": "int8"},
"openvino-cpu": {"model": "medium", "compute_type": "int8"},
}
# Одно место правды для допустимых ключей конфига
_VALID_KEYS = set(HARDCODED_DEFAULTS)
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino"}
_VALID_DEVICES = {"auto", "cpu", "cuda", "openvino", "openvino-gpu", "openvino-cpu"}
def find_config_file() -> Path | None:
+7 -1
View File
@@ -36,6 +36,12 @@ def load_model(
try:
_notify_status(on_status, f"Инициализирую модель на {device}...")
model = backend.create_model(model_path, device, compute_type)
# Резолвим actual_device по реальному OpenVINO device
ov_dev = getattr(backend, "actual_ov_device", None)
if ov_dev == "GPU" and actual_device != "openvino-gpu":
actual_device = "openvino-gpu"
elif ov_dev == "CPU" and actual_device.startswith("openvino") and actual_device != "openvino-cpu":
actual_device = "openvino-cpu"
except (RuntimeError, ValueError) as exc:
if device != "cpu" and _is_backend_error(exc, device):
if strict_device:
@@ -157,7 +163,7 @@ def _is_backend_error(exc: BaseException, device: str) -> bool:
"""Определяет, связана ли ошибка с конкретным бэкендом (а не с пользовательскими данными)."""
if device in ("cuda", "cpu"):
return _is_cuda_error(exc)
if device == "openvino":
if device.startswith("openvino"):
return _is_openvino_error(exc)
return False
+1 -1
View File
@@ -18,7 +18,7 @@ class TranscribeResult:
language: str
language_probability: float
duration: float # seconds
device_used: str # "cpu" / "cuda" / "openvino"
device_used: str # "cpu" / "cuda" / "openvino-gpu" / "openvino-cpu"
@dataclass
+42 -9
View File
@@ -16,16 +16,23 @@ SUPPORTED_EXTENSIONS = {
def detect_device(requested: str = "auto") -> str:
"""Определяет устройство для вычислений.
При ``requested="auto"`` проверяет: CUDA OpenVINO CPU.
Явное значение возвращается как есть.
При ``requested="auto"`` проверяет: CUDA OpenVINO GPU OpenVINO CPU CPU.
``"openvino"`` алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU).
Возвращает всегда конкретное значение (не абстрактный ``"openvino"``).
"""
if requested != "auto":
return requested
if shutil.which("nvidia-smi") is not None:
return "cuda"
if _is_openvino_available():
return "openvino"
return "cpu"
if requested == "auto":
if shutil.which("nvidia-smi") is not None:
return "cuda"
if _is_openvino_gpu_available():
return "openvino-gpu"
if _is_openvino_available():
return "openvino-cpu"
return "cpu"
if requested == "openvino":
if _is_openvino_gpu_available():
return "openvino-gpu"
return "openvino-cpu"
return requested
def _is_openvino_available() -> bool:
@@ -40,6 +47,32 @@ def _is_openvino_available() -> bool:
return False
def _is_openvino_gpu_available() -> bool:
"""Проверяет доступность Intel GPU через OpenVINO.
Сохраняет x86/AMD64 gate и глотает все ошибки импорта/runtime.
"""
if not _is_openvino_available():
return False
try:
from openvino import Core
return "GPU" in Core().available_devices
except Exception:
return False
def get_intel_gpu_name() -> str | None:
"""Возвращает название Intel GPU через OpenVINO (для метаданных транскрипта)."""
try:
from openvino import Core
name = Core().get_property("GPU", "FULL_DEVICE_NAME")
return name if name else None
except Exception:
return None
def get_gpu_name() -> str | None:
"""Возвращает название GPU через ``nvidia-smi`` (для метаданных транскрипта)."""
try: