refactor(cli): одиночный файл и батч переведены на module выполнения
CLI строит один ExecutionRequest, создаёт Transcriber после prescan и получает результат с ExecutionInfo: разрешение auto, device-aware умолчания, проверка пословного контракта, строка устройства для шапки и fallback ушли за seam. --verbose печатает движок, потоки и runtime_info(). Удалены load_model, _transcribe_file, TranscribeFileResult, apply_device_defaults и тесты переноса полей; сценарные тесты CLI идут через CliRunner с fake adapter на get_backend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e22b2f1bae
commit
0f8ecb5bd3
13 files changed
+402
-1606
No files matched your search
@@ -167,7 +167,6 @@ class FasterWhisperBackend:
|
||||
except Exception as exc: # noqa: BLE001 — диагностика не должна ронять запуск
|
||||
cuda_devices = f"недоступно ({exc})"
|
||||
return {
|
||||
"engine": "faster-whisper",
|
||||
"faster_whisper": package_version("faster-whisper"),
|
||||
"ctranslate2": package_version("ctranslate2"),
|
||||
"cuda_devices": cuda_devices,
|
||||
|
||||
@@ -229,7 +229,6 @@ class OnnxAsrBackend:
|
||||
|
||||
configured = ", ".join(self._providers)
|
||||
return {
|
||||
"engine": "onnx-asr",
|
||||
"onnxruntime": package_version("onnxruntime"),
|
||||
"onnx_asr": package_version("onnx-asr"),
|
||||
"available_providers": ", ".join(onnxruntime.get_available_providers()),
|
||||
|
||||
@@ -209,7 +209,6 @@ class OpenVINOBackend:
|
||||
from openvino import Core
|
||||
|
||||
return {
|
||||
"engine": "openvino",
|
||||
"openvino": package_version("openvino"),
|
||||
"available_devices": ", ".join(Core().available_devices),
|
||||
"device": self.actual_ov_device or "",
|
||||
|
||||
+90
-179
@@ -13,8 +13,6 @@ from rich.status import Status
|
||||
|
||||
from .config import (
|
||||
CliValues,
|
||||
ResolvedConfig,
|
||||
apply_device_defaults,
|
||||
load_config,
|
||||
resolve_defaults,
|
||||
)
|
||||
@@ -39,11 +37,12 @@ from .quality import (
|
||||
)
|
||||
from .speaker_diarizer import SpeakerDiarizer, load_speaker_diarizer
|
||||
from .transcriber import (
|
||||
ExecutionInfo,
|
||||
ExecutionRequest,
|
||||
Segment,
|
||||
Transcriber,
|
||||
TranscribeResult,
|
||||
_transcribe_file,
|
||||
cuda_error_hint,
|
||||
load_model,
|
||||
)
|
||||
from .types import (
|
||||
UNKNOWN_LANGUAGE,
|
||||
@@ -53,10 +52,7 @@ from .types import (
|
||||
)
|
||||
from .utils import (
|
||||
build_output_path,
|
||||
detect_device,
|
||||
expand_globs,
|
||||
get_gpu_name,
|
||||
get_intel_gpu_name,
|
||||
has_existing_transcript,
|
||||
validate_input_file,
|
||||
)
|
||||
@@ -95,21 +91,6 @@ def _with_cli_warning_renderer(
|
||||
return wrapped
|
||||
|
||||
|
||||
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-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)"
|
||||
if device_used == "onnx":
|
||||
return "ONNX (CPU)"
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _format_language_mode(requested_language: str, result: TranscribeResult) -> str:
|
||||
"""Описывает источник языка, не выдавая профиль модели за детектор."""
|
||||
if requested_language != "auto":
|
||||
@@ -348,7 +329,7 @@ def main(
|
||||
)
|
||||
raise SystemExit(2)
|
||||
|
||||
resolved_device: str | None = None
|
||||
requested_device: str | None = None
|
||||
try:
|
||||
config = load_config()
|
||||
cli_values: CliValues = {
|
||||
@@ -360,11 +341,19 @@ def main(
|
||||
}
|
||||
defaults = resolve_defaults(cli_values, config)
|
||||
diarize_enabled = defaults["diarize"] is True or speakers is not None
|
||||
requested_device = defaults["device"]
|
||||
|
||||
resolved_device = detect_device(defaults["device"])
|
||||
defaults = apply_device_defaults(defaults, resolved_device, cli_values, config)
|
||||
|
||||
ct_explicit = compute_type is not None or "compute_type" in config
|
||||
# Явные model/compute_type; умолчания исполнения разрешает module.
|
||||
request = ExecutionRequest(
|
||||
device=requested_device,
|
||||
model=model if model is not None else config.get("model"),
|
||||
compute_type=(
|
||||
compute_type if compute_type is not None else config.get("compute_type")
|
||||
),
|
||||
language=defaults["language"],
|
||||
cpu_threads=threads,
|
||||
require_word_timestamps=diarize_enabled,
|
||||
)
|
||||
|
||||
expanded = expand_globs(files)
|
||||
if not expanded:
|
||||
@@ -381,22 +370,18 @@ def main(
|
||||
if is_batch:
|
||||
_run_batch(
|
||||
expanded,
|
||||
defaults,
|
||||
request,
|
||||
verbose,
|
||||
force,
|
||||
ct_explicit,
|
||||
cpu_threads=threads,
|
||||
diarize=diarize_enabled,
|
||||
speakers=speakers,
|
||||
)
|
||||
else:
|
||||
_run_single(
|
||||
expanded[0],
|
||||
defaults,
|
||||
request,
|
||||
output,
|
||||
verbose,
|
||||
ct_explicit,
|
||||
cpu_threads=threads,
|
||||
diarize=diarize_enabled,
|
||||
speakers=speakers,
|
||||
)
|
||||
@@ -406,15 +391,15 @@ def main(
|
||||
except SystemExit:
|
||||
raise
|
||||
except ValueError as exc:
|
||||
_print_cuda_hint(exc, resolved_device)
|
||||
_print_cuda_hint(exc, requested_device)
|
||||
console.print(f"Ошибка: {exc}", style="red bold")
|
||||
raise SystemExit(1)
|
||||
except (FileNotFoundError,) as exc:
|
||||
_print_cuda_hint(exc, resolved_device)
|
||||
_print_cuda_hint(exc, requested_device)
|
||||
console.print(f"Ошибка: {exc}", style="red bold")
|
||||
raise SystemExit(1)
|
||||
except Exception as exc:
|
||||
_print_cuda_hint(exc, resolved_device)
|
||||
_print_cuda_hint(exc, requested_device)
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
else:
|
||||
@@ -423,13 +408,48 @@ def main(
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def _print_execution_header(info: ExecutionInfo, verbose: bool) -> None:
|
||||
"""Печатает выбранное исполнение; в --verbose — версии runtime для диагностики."""
|
||||
console.print(
|
||||
f"Модель: [bold]{info.model}[/bold] "
|
||||
f"Устройство: [bold]{info.device}[/bold] "
|
||||
f"Compute: [bold]{info.compute_type}[/bold]"
|
||||
)
|
||||
if info.device == "openvino-gpu" and info.model != "large-v3":
|
||||
console.print(
|
||||
"Совет: --model large-v3 даёт лучшее качество на GPU (~2x дольше)",
|
||||
style="dim",
|
||||
)
|
||||
if info.device != info.resolved_device:
|
||||
console.print(
|
||||
f"Запрошено {info.requested_device}, используется {info.device}",
|
||||
style="yellow",
|
||||
)
|
||||
if verbose:
|
||||
threads = info.cpu_threads or "по умолчанию библиотеки"
|
||||
console.print(f"Движок: {info.engine} Потоки: {threads}", style="dim")
|
||||
for key, value in info.runtime.items():
|
||||
console.print(f" {key}: {value}", style="dim", markup=False)
|
||||
|
||||
|
||||
def _load_diarizer(
|
||||
diarize: bool, speakers: int | None, cpu_threads: int
|
||||
) -> SpeakerDiarizer | None:
|
||||
"""Готовит диаризатор до первого ASR; пословный контракт уже проверен module."""
|
||||
if not diarize:
|
||||
return None
|
||||
return load_speaker_diarizer(
|
||||
speakers=speakers,
|
||||
threads=cpu_threads,
|
||||
on_status=lambda message: console.print(message),
|
||||
)
|
||||
|
||||
|
||||
def _run_single(
|
||||
file: Path,
|
||||
defaults: ResolvedConfig,
|
||||
request: ExecutionRequest,
|
||||
output: Path | None,
|
||||
verbose: bool,
|
||||
compute_type_explicit: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
diarize: bool = False,
|
||||
speakers: int | None = None,
|
||||
) -> None:
|
||||
@@ -437,9 +457,6 @@ def _run_single(
|
||||
start = time.monotonic()
|
||||
|
||||
validated_file = validate_input_file(file)
|
||||
requested_device = defaults["device"]
|
||||
resolved_device = detect_device(requested_device)
|
||||
strict = requested_device != "auto"
|
||||
output_path = build_output_path(validated_file, output)
|
||||
|
||||
console.print(f"Файл: [bold]{validated_file.name}[/bold]")
|
||||
@@ -447,59 +464,18 @@ def _run_single(
|
||||
def on_segment(seg: Segment) -> None:
|
||||
console.print(f" [{seg.start:.2f}s] {seg.text.strip()}")
|
||||
|
||||
model_obj, actual_device, backend, model_path = load_model(
|
||||
defaults["model"],
|
||||
resolved_device,
|
||||
defaults["compute_type"],
|
||||
on_status=lambda msg: console.print(msg),
|
||||
strict_device=strict,
|
||||
compute_type_explicit=compute_type_explicit,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
actual_ct = (
|
||||
getattr(backend, "actual_compute_type", defaults["compute_type"])
|
||||
or defaults["compute_type"]
|
||||
)
|
||||
console.print(
|
||||
f"Модель: [bold]{defaults['model']}[/bold] "
|
||||
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",
|
||||
)
|
||||
|
||||
speaker_diarizer = None
|
||||
if diarize:
|
||||
if not backend.word_timestamps_available:
|
||||
raise ValueError(
|
||||
"Выбранный движок или модель не поддерживает пословные таймкоды"
|
||||
)
|
||||
speaker_diarizer = load_speaker_diarizer(
|
||||
speakers=speakers,
|
||||
threads=cpu_threads,
|
||||
on_status=lambda message: console.print(message),
|
||||
)
|
||||
transcriber = Transcriber(request)
|
||||
info = transcriber.prepare(on_status=lambda msg: console.print(msg))
|
||||
_print_execution_header(info, verbose)
|
||||
speaker_diarizer = _load_diarizer(diarize, speakers, request.cpu_threads)
|
||||
|
||||
with Status("Подготавливаю запуск...", console=console) as status:
|
||||
tfr = _transcribe_file(
|
||||
model=model_obj,
|
||||
actual_device=actual_device,
|
||||
backend=backend,
|
||||
model_path=model_path,
|
||||
file_path=validated_file,
|
||||
model_name=defaults["model"],
|
||||
compute_type=defaults["compute_type"],
|
||||
language=defaults["language"] if defaults["language"] != "auto" else None,
|
||||
result = transcriber.transcribe(
|
||||
validated_file,
|
||||
on_segment=on_segment if verbose else None,
|
||||
on_status=status.update,
|
||||
strict_device=strict,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
result = tfr.result
|
||||
speaker_transcript = None
|
||||
diarization_warning = None
|
||||
diarization_degraded = False
|
||||
@@ -526,18 +502,12 @@ def _run_single(
|
||||
if diarization_warning is not None:
|
||||
console.print(f"Внимание: {diarization_warning}", style="yellow")
|
||||
|
||||
if tfr.actual_device != resolved_device:
|
||||
if requested_device == "auto":
|
||||
console.print(
|
||||
f"Определено устройство {resolved_device}, "
|
||||
f"но использовано {tfr.actual_device} (fallback)",
|
||||
style="yellow",
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"Запрошено {requested_device}, использовано {tfr.actual_device}",
|
||||
style="yellow",
|
||||
)
|
||||
execution = transcriber.execution
|
||||
if execution.device != info.device:
|
||||
console.print(
|
||||
f"Запрошено {execution.requested_device}, использовано {execution.device}",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
if len(result.segments) == 0:
|
||||
message = f"Речь не обнаружена в файле {validated_file.name}"
|
||||
@@ -545,14 +515,13 @@ def _run_single(
|
||||
message += "; диаризация не запускалась"
|
||||
console.print(message, style="yellow")
|
||||
|
||||
device_info = _format_device_info(result.device_used)
|
||||
language_mode = _format_language_mode(defaults["language"], result)
|
||||
language_mode = _format_language_mode(request.language or "auto", result)
|
||||
|
||||
content = format_transcript(
|
||||
result=result,
|
||||
source_filename=validated_file.name,
|
||||
model_name=defaults["model"],
|
||||
device_info=device_info,
|
||||
model_name=execution.model,
|
||||
device_info=execution.description,
|
||||
language_mode=language_mode,
|
||||
speaker_transcript=speaker_transcript,
|
||||
diarization_warning=diarization_warning,
|
||||
@@ -569,11 +538,9 @@ def _run_single(
|
||||
|
||||
def _run_batch(
|
||||
files: list[Path],
|
||||
defaults: ResolvedConfig,
|
||||
request: ExecutionRequest,
|
||||
verbose: bool,
|
||||
force: bool,
|
||||
compute_type_explicit: bool = False,
|
||||
cpu_threads: int = 0,
|
||||
diarize: bool = False,
|
||||
speakers: int | None = None,
|
||||
) -> None:
|
||||
@@ -603,50 +570,11 @@ def _run_batch(
|
||||
raise SystemExit(1)
|
||||
return
|
||||
|
||||
# Phase 2: Load model (ensure + create в одном вызове)
|
||||
requested_device = defaults["device"]
|
||||
resolved_device = detect_device(requested_device)
|
||||
strict = requested_device != "auto"
|
||||
model_obj, actual_device, backend, model_path = load_model(
|
||||
defaults["model"],
|
||||
resolved_device,
|
||||
defaults["compute_type"],
|
||||
on_status=lambda msg: console.print(msg),
|
||||
strict_device=strict,
|
||||
compute_type_explicit=compute_type_explicit,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
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(
|
||||
f"Определено устройство {resolved_device}, "
|
||||
f"но используется {actual_device} (fallback)",
|
||||
style="yellow",
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"Запрошено {requested_device}, используется {actual_device}",
|
||||
style="yellow",
|
||||
)
|
||||
|
||||
speaker_diarizer = None
|
||||
if diarize:
|
||||
if not backend.word_timestamps_available:
|
||||
raise ValueError(
|
||||
"Выбранный движок или модель не поддерживает пословные таймкоды"
|
||||
)
|
||||
speaker_diarizer = load_speaker_diarizer(
|
||||
speakers=speakers,
|
||||
threads=cpu_threads,
|
||||
on_status=lambda message: console.print(message),
|
||||
)
|
||||
# Phase 2: module выполнения создаётся, когда есть работа
|
||||
transcriber = Transcriber(request)
|
||||
info = transcriber.prepare(on_status=lambda msg: console.print(msg))
|
||||
_print_execution_header(info, verbose)
|
||||
speaker_diarizer = _load_diarizer(diarize, speakers, request.cpu_threads)
|
||||
|
||||
# Phase 3: Transcribe
|
||||
processed = 0
|
||||
@@ -663,39 +591,24 @@ def _run_batch(
|
||||
def on_segment(seg: Segment) -> None:
|
||||
console.print(f" [{seg.start:.2f}s] {seg.text.strip()}")
|
||||
|
||||
device_before = transcriber.execution.device
|
||||
with Status(f"{prefix}...", console=console) as status:
|
||||
tfr = _transcribe_file(
|
||||
model=model_obj,
|
||||
actual_device=actual_device,
|
||||
backend=backend,
|
||||
model_path=model_path,
|
||||
file_path=file,
|
||||
model_name=defaults["model"],
|
||||
compute_type=defaults["compute_type"],
|
||||
language=defaults["language"]
|
||||
if defaults["language"] != "auto"
|
||||
else None,
|
||||
result = transcriber.transcribe(
|
||||
file,
|
||||
on_segment=on_segment if verbose else None,
|
||||
on_status=status.update
|
||||
if not verbose
|
||||
else lambda msg: console.print(msg),
|
||||
strict_device=strict,
|
||||
cpu_threads=cpu_threads,
|
||||
)
|
||||
|
||||
if tfr.actual_device != actual_device:
|
||||
execution = transcriber.execution
|
||||
if execution.device != device_before:
|
||||
console.print(
|
||||
f" {file.name}: fallback на {tfr.actual_device} при транскрипции",
|
||||
f" {file.name}: fallback на {execution.device} при транскрипции",
|
||||
style="yellow",
|
||||
)
|
||||
# Обновляем после возможного mid-stream fallback
|
||||
model_obj = tfr.model
|
||||
actual_device = tfr.actual_device
|
||||
backend = tfr.backend
|
||||
model_path = tfr.model_path
|
||||
|
||||
result = tfr.result
|
||||
language_mode = _format_language_mode(defaults["language"], result)
|
||||
language_mode = _format_language_mode(request.language or "auto", result)
|
||||
speaker_transcript = None
|
||||
diarization_warning = None
|
||||
file_degraded = False
|
||||
@@ -735,13 +648,11 @@ def _run_batch(
|
||||
message += "; диаризация не запускалась"
|
||||
console.print(message, style="yellow")
|
||||
|
||||
device_info = _format_device_info(result.device_used)
|
||||
|
||||
content = format_transcript(
|
||||
result=result,
|
||||
source_filename=file.name,
|
||||
model_name=defaults["model"],
|
||||
device_info=device_info,
|
||||
model_name=execution.model,
|
||||
device_info=execution.description,
|
||||
language_mode=language_mode,
|
||||
speaker_transcript=speaker_transcript,
|
||||
diarization_warning=diarization_warning,
|
||||
@@ -760,7 +671,7 @@ def _run_batch(
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception as exc:
|
||||
_print_cuda_hint(exc, actual_device)
|
||||
_print_cuda_hint(exc, request.device)
|
||||
if verbose:
|
||||
console.print_exception()
|
||||
else:
|
||||
|
||||
@@ -171,23 +171,3 @@ def _resolve_value(cli_value: _T | None, config_value: _T | None, default: _T) -
|
||||
if config_value is not None:
|
||||
return config_value
|
||||
return default
|
||||
|
||||
|
||||
def apply_device_defaults(
|
||||
defaults: ResolvedConfig,
|
||||
resolved_device: str,
|
||||
cli_values: CliValues,
|
||||
config: ConfigValues,
|
||||
) -> ResolvedConfig:
|
||||
"""Применяет device-aware дефолты для model и compute_type,
|
||||
если они не были явно заданы через CLI или конфиг."""
|
||||
device_defs = DEVICE_DEFAULTS.get(resolved_device, {})
|
||||
if not device_defs:
|
||||
return defaults
|
||||
|
||||
result = dict(defaults)
|
||||
for key in ("model", "compute_type"):
|
||||
if cli_values.get(key) is None and key not in config:
|
||||
if key in device_defs:
|
||||
result[key] = device_defs[key]
|
||||
return cast(ResolvedConfig, result)
|
||||
@@ -10,9 +10,8 @@ from local_transcriber.backends import get_backend
|
||||
from local_transcriber.config import DEVICE_DEFAULTS, HARDCODED_DEFAULTS
|
||||
|
||||
# Re-export из types.py для обратной совместимости
|
||||
from local_transcriber.types import (
|
||||
from local_transcriber.types import ( # noqa: F401
|
||||
Segment,
|
||||
TranscribeFileResult,
|
||||
TranscribeResult,
|
||||
WordTimestampsUnavailableError,
|
||||
)
|
||||
@@ -228,120 +227,6 @@ def _describe_device(device: str) -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
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 | None = None,
|
||||
@@ -377,8 +262,6 @@ def ensure_model_available(
|
||||
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(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
# Единый признак «язык неизвестен» для всех бэкендов
|
||||
UNKNOWN_LANGUAGE = "unknown"
|
||||
@@ -83,14 +82,5 @@ class TranscribeResult:
|
||||
words: list[Word] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TranscribeFileResult:
|
||||
result: TranscribeResult
|
||||
model: Any # backend-specific model handle
|
||||
actual_device: str
|
||||
backend: Any = None # backend instance (для переиспользования в батче)
|
||||
model_path: str = "" # путь к модели (меняется при cross-backend fallback)
|
||||
|
||||
|
||||
StatusCallback = Callable[[str], None] | None
|
||||
SegmentCallback = Callable[[Segment], None] | None
|
||||
Reference in new issue
Block a user