Files
local-transcriber/src/local_transcriber/transcriber.py
T
ddadminandClaude Opus 4.6 857267763c feat(cli): --threads для управления CPU-потоками + бенчмарк CUDA compute_type
- Зачем:
  - CTranslate2 по умолчанию использует 4 потока; на многоядерных CPU (8+ ядер)
    это неоптимально — --threads 8 даёт +13% ускорения.
  - Не было данных по int8_float32/int8_float16 на NVIDIA GPU.

- Что:
  - --threads / -t: новый CLI-флаг, пробрасывается через load_model →
    backend.create_model(cpu_threads=...) → WhisperModel(cpu_threads=...).
  - Валидация min=0 на входе (typer), Backend протокол синхронизирован.
  - docs/gpu.md: результаты бенчмарка 6 комбинаций CUDA compute_type
    (medium/large-v3 × float16/int8_float32/int8_float16) на двух файлах
    (16 мин и 46 мин). Ключевой вывод: float16 — оптимальный дефолт;
    large-v3 ненадёжен на длинных записях.
  - README: --threads добавлен в таблицу опций.
  - Фикс теста: test_resolve_repo_explicit_unsupported_pair_raises обновлён
    под добавление medium fp16 модели.

- Проверка:
  - uv run pytest: 157 passed.
  - transcribe file.mp4 --device cpu --threads 8: 277с vs 320с (дефолт).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 22:49:16 +03:00

191 lines
7.6 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.
"""Оркестрация транскрипции: выбор бэкенда, загрузка модели, fallback."""
import warnings
from collections.abc import Callable
from pathlib import Path
from typing import Any
from local_transcriber.backends import get_backend
# Re-export из types.py для обратной совместимости
from local_transcriber.types import ( # noqa: F401
Segment,
TranscribeFileResult,
TranscribeResult,
)
def load_model(
model_name: str,
device: str,
compute_type: str,
on_status: Callable[[str], None] | None = None,
strict_device: bool = False,
compute_type_explicit: bool = False,
cpu_threads: int = 0,
) -> tuple[Any, str, Any, str]:
"""Загружает модель: ensure + create с fallback.
Возвращает (model, actual_device, backend, model_path).
compute_type_explicit: True если пользователь явно указал --compute-type.
cpu_threads: число потоков для CPU inference (0 = дефолт библиотеки).
"""
backend = get_backend(device, compute_type_explicit=compute_type_explicit)
actual_device = device
model_path = backend.ensure_model_available(model_name, compute_type, on_status)
try:
_notify_status(on_status, f"Инициализирую модель на {device}...")
model = backend.create_model(model_path, device, compute_type, cpu_threads=cpu_threads)
# Резолвим 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:
raise
warnings.warn(
f"Не удалось загрузить модель на {device}: {exc}. "
"Переключение на CPU.",
stacklevel=2,
)
actual_device = "cpu"
backend = get_backend("cpu")
model_path = backend.ensure_model_available(model_name, compute_type, on_status)
_notify_status(on_status, "Инициализирую модель на cpu...")
model = backend.create_model(model_path, "cpu", compute_type, cpu_threads=cpu_threads)
else:
raise
return model, actual_device, backend, model_path
def _transcribe_file(
model: Any,
actual_device: str,
backend: Any,
model_path: str,
file_path: Path,
model_name: str,
compute_type: str,
language: str | None = None,
on_segment: Callable[[Segment], None] | None = None,
on_status: Callable[[str], None] | None = None,
strict_device: bool = False,
cpu_threads: int = 0,
) -> TranscribeFileResult:
"""Транскрибирует один файл. При mid-stream fallback перезагружает модель."""
lang_arg = language if language and language != "auto" else None
try:
_notify_status(on_status, "Транскрибирую...")
result = backend.transcribe(model, file_path, lang_arg, on_segment, on_status)
result.device_used = actual_device
except (RuntimeError, ValueError) as exc:
if actual_device != "cpu" and _is_backend_error(exc, actual_device):
if strict_device:
raise
warnings.warn(
f"Ошибка при транскрипции на {actual_device}: {exc}. "
"Переключение на CPU и повтор.",
stacklevel=2,
)
actual_device = "cpu"
backend = get_backend("cpu")
model_path = backend.ensure_model_available(model_name, compute_type, on_status)
_notify_status(on_status, "Инициализирую модель на cpu...")
model = backend.create_model(model_path, "cpu", compute_type, cpu_threads=cpu_threads)
_notify_status(on_status, "Транскрибирую...")
result = backend.transcribe(model, file_path, lang_arg, on_segment, on_status)
result.device_used = actual_device
else:
raise
return TranscribeFileResult(
result=result,
model=model,
actual_device=actual_device,
backend=backend,
model_path=model_path,
)
def transcribe(
file_path: Path,
model_name: str = "large-v3",
device: str = "auto",
compute_type: str = "int8",
language: str | None = None,
on_segment: Callable[[Segment], None] | None = None,
on_status: Callable[[str], None] | None = None,
strict_device: bool = False,
cpu_threads: int = 0,
) -> TranscribeResult:
"""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
cpu_threads=cpu_threads,
)
tfr = _transcribe_file(
model, actual_device, backend, model_path,
file_path, model_name, compute_type,
language, on_segment, on_status, strict_device,
cpu_threads=cpu_threads,
)
return tfr.result
def ensure_model_available(
model_name: str,
device: str = "cpu",
compute_type: str | None = None,
on_status: Callable[[str], None] | None = None,
) -> str:
"""Публичный helper: гарантирует наличие модели для указанного бэкенда."""
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)
def _is_cuda_error(exc: BaseException) -> bool:
"""Проверка CUDA ошибок — используется в cli.py для Windows-диагностики."""
msg = str(exc).lower()
return any(k in msg for k in ("cuda", "cublas", "cudnn", "out of memory"))
def _is_backend_error(exc: BaseException, device: str) -> bool:
"""Определяет, связана ли ошибка с конкретным бэкендом (а не с пользовательскими данными)."""
if device in ("cuda", "cpu"):
return _is_cuda_error(exc)
if device.startswith("openvino"):
return _is_openvino_error(exc)
return False
def _is_openvino_error(exc: BaseException) -> bool:
"""Проверка ошибок 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:
if on_status is not None:
on_status(message)