Files
local-transcriber/src/local_transcriber/utils.py
T
Dmitry DementievandClaude Opus 4.6 617181f2dc 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>
2026-03-22 15:45:27 +03:00

149 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Утилиты для валидации входных файлов, определения устройства и работы с путями."""
import glob
import platform
import shutil
import subprocess
import warnings
from pathlib import Path
SUPPORTED_EXTENSIONS = {
".mp3", ".wav", ".flac", ".ogg", ".m4a", ".wma", ".aac",
".mp4", ".mkv", ".avi", ".mov", ".webm", ".ts",
}
def detect_device(requested: str = "auto") -> str:
"""Определяет устройство для вычислений.
При ``requested="auto"`` проверяет: CUDA → OpenVINO GPU → OpenVINO CPU → CPU.
``"openvino"`` — алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU).
Возвращает всегда конкретное значение (не абстрактный ``"openvino"``).
"""
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:
"""Проверяет доступность OpenVINO: x86/AMD64 архитектура + пакет установлен."""
if platform.machine().lower() not in {"x86_64", "amd64"}:
return False
try:
import openvino_genai # noqa: F401
return True
except ImportError:
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:
result = subprocess.run(
["nvidia-smi", "--query-gpu=name", "--format=csv,noheader"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0:
lines = result.stdout.strip().splitlines()
if lines:
name = lines[0].strip()
return name if name else None
except FileNotFoundError:
pass
return None
def validate_input_file(path: Path) -> Path:
"""Проверяет существование, непустоту и расширение файла.
Неизвестное расширение — предупреждение, а не ошибка: файл может оказаться
корректным контейнером, просто с нестандартным суффиксом.
Возвращает resolved-путь.
"""
if not path.exists():
raise FileNotFoundError(f"Файл не найден: {path}")
if not path.is_file():
raise ValueError(f"Путь не является файлом: {path}")
if path.stat().st_size == 0:
raise ValueError(f"Файл пустой: {path}")
if path.suffix.lower() not in SUPPORTED_EXTENSIONS:
warnings.warn(
f"Расширение '{path.suffix}' не входит в список поддерживаемых. "
"Попытка продолжить.",
stacklevel=2,
)
return path.resolve()
def build_output_path(input_path: Path, output: Path | None = None) -> Path:
"""Строит путь выходного файла: ``<имя>-transcript.md`` рядом с исходным."""
if output is not None:
return output
return input_path.with_stem(input_path.stem + "-transcript").with_suffix(".md")
def expand_globs(paths: list[Path]) -> list[Path]:
"""Раскрывает glob-паттерны в списке путей с дедупликацией.
Typer на Windows не раскрывает ``*.mp4`` самостоятельно,
поэтому глобы обрабатываются вручную. Дедупликация — по resolved-пути.
"""
seen: set[Path] = set()
result: list[Path] = []
for p in paths:
s = str(p)
if any(c in s for c in ("*", "?", "[")):
candidates = [Path(m) for m in sorted(glob.glob(s))]
else:
candidates = [p]
for c in candidates:
resolved = c.resolve()
if resolved not in seen:
seen.add(resolved)
result.append(c)
return result
def has_existing_transcript(input_path: Path) -> bool:
"""Проверяет наличие транскрипта для skip-логики батч-режима."""
return build_output_path(input_path).exists()