- Зачем:
- 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>
191 lines
7.6 KiB
Python
191 lines
7.6 KiB
Python
"""Оркестрация транскрипции: выбор бэкенда, загрузка модели, 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)
|