fix(auto)!: учтены ограничения ONNX-профиля
- Зачем: - после смены auto-профиля пользователю нужны явные предупреждения о языковых ограничениях и рабочие пути выбора Whisper-бэкенда. - Что: - в MODEL_CATALOG добавлены поддерживаемые языки ONNX-моделей и предупреждение о несовместимом языке. - для Whisper-имён добавлены подсказки с совместимыми парами backend/model. - вывод языка различает детекцию, язык моноязычной модели и отсутствие детекции. - усилены тесты implicit compute type и turbo-каталога, удалены дубли auto-тестов. - Проверка: - uv run pytest -q: 241 passed, 1 skipped. - uv lock --check, compileall и git diff --check. BREAKING CHANGE: --device auto без NVIDIA теперь выбирает ONNX GigaAM RNN-T; прежний профиль возвращается явным --device или конфигурацией.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -12,35 +13,77 @@ from local_transcriber.types import Segment, TranscribeResult
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OnnxModelSpec:
|
||||
"""Имя onnx-asr и опубликованные варианты квантизации модели."""
|
||||
"""Имя onnx-asr, варианты квантизации и поддерживаемые языки."""
|
||||
|
||||
model_id: str
|
||||
quantizations: frozenset[str | None]
|
||||
supported_languages: frozenset[str]
|
||||
|
||||
|
||||
_INT8_AND_FLOAT32 = frozenset({"int8", None})
|
||||
_RUSSIAN_ONLY = frozenset({"ru"})
|
||||
_GIGAAM_MULTILINGUAL_LANGUAGES = frozenset({"ru", "en", "kk", "ky", "uz"})
|
||||
_PARAKEET_V3_LANGUAGES = frozenset(
|
||||
{
|
||||
"bg",
|
||||
"hr",
|
||||
"cs",
|
||||
"da",
|
||||
"nl",
|
||||
"en",
|
||||
"et",
|
||||
"fi",
|
||||
"fr",
|
||||
"de",
|
||||
"el",
|
||||
"hu",
|
||||
"it",
|
||||
"lv",
|
||||
"lt",
|
||||
"mt",
|
||||
"pl",
|
||||
"pt",
|
||||
"ro",
|
||||
"sk",
|
||||
"sl",
|
||||
"es",
|
||||
"sv",
|
||||
"ru",
|
||||
"uk",
|
||||
}
|
||||
)
|
||||
_WHISPER_MODEL_NAMES = frozenset(
|
||||
{"tiny", "base", "small", "medium", "large-v3", "large-v3-turbo"}
|
||||
)
|
||||
|
||||
MODEL_CATALOG: dict[str, OnnxModelSpec] = {
|
||||
"gigaam-v3": OnnxModelSpec("gigaam-v3-ctc", _INT8_AND_FLOAT32),
|
||||
"gigaam-v3": OnnxModelSpec(
|
||||
"gigaam-v3-ctc", _INT8_AND_FLOAT32, _RUSSIAN_ONLY
|
||||
),
|
||||
"parakeet-v3": OnnxModelSpec(
|
||||
"nemo-parakeet-tdt-0.6b-v3",
|
||||
_INT8_AND_FLOAT32,
|
||||
_PARAKEET_V3_LANGUAGES,
|
||||
),
|
||||
"gigaam-multilingual-ctc": OnnxModelSpec(
|
||||
"gigaam-multilingual-ctc",
|
||||
_INT8_AND_FLOAT32,
|
||||
_GIGAAM_MULTILINGUAL_LANGUAGES,
|
||||
),
|
||||
"gigaam-multilingual-large-ctc": OnnxModelSpec(
|
||||
"gigaam-multilingual-large-ctc",
|
||||
_INT8_AND_FLOAT32,
|
||||
_GIGAAM_MULTILINGUAL_LANGUAGES,
|
||||
),
|
||||
"gigaam-v3-e2e-ctc": OnnxModelSpec(
|
||||
"gigaam-v3-e2e-ctc",
|
||||
_INT8_AND_FLOAT32,
|
||||
_RUSSIAN_ONLY,
|
||||
),
|
||||
"gigaam-v3-e2e-rnnt": OnnxModelSpec(
|
||||
"gigaam-v3-e2e-rnnt",
|
||||
_INT8_AND_FLOAT32,
|
||||
_RUSSIAN_ONLY,
|
||||
),
|
||||
}
|
||||
|
||||
@@ -85,6 +128,8 @@ class OnnxAsrBackend:
|
||||
self._compute_type_explicit = compute_type_explicit
|
||||
self.actual_compute_type: str | None = None
|
||||
self._resolved_model_id: str | None = None
|
||||
self._model_name: str | None = None
|
||||
self._model_spec: OnnxModelSpec | None = None
|
||||
self._vad: Any = None
|
||||
|
||||
def ensure_model_available(
|
||||
@@ -119,6 +164,8 @@ class OnnxAsrBackend:
|
||||
|
||||
self.actual_compute_type = resolved_compute_type
|
||||
self._resolved_model_id = self._resolve_model(model_name)
|
||||
self._model_name = model_name
|
||||
self._model_spec = spec
|
||||
return self._resolved_model_id
|
||||
|
||||
def create_model(
|
||||
@@ -162,13 +209,14 @@ class OnnxAsrBackend:
|
||||
"""
|
||||
from faster_whisper import decode_audio
|
||||
|
||||
self._warn_if_language_unsupported(language)
|
||||
_notify(on_status, "Загружаю аудио...")
|
||||
audio_array = decode_audio(str(file_path), sampling_rate=16000)
|
||||
duration = len(audio_array) / 16000.0
|
||||
|
||||
_notify(on_status, "Транскрибирую (onnx-asr)...")
|
||||
segments: list[Segment] = []
|
||||
detected_language = language or "unknown"
|
||||
result_language = language or _model_language(self._model_spec) or "unknown"
|
||||
|
||||
for vad_seg in model.recognize(
|
||||
audio_array, sample_rate=16000, language=language
|
||||
@@ -192,7 +240,7 @@ class OnnxAsrBackend:
|
||||
|
||||
return TranscribeResult(
|
||||
segments=segments,
|
||||
language=detected_language,
|
||||
language=result_language,
|
||||
language_probability=1.0 if language else 0.0,
|
||||
duration=duration,
|
||||
device_used="", # оркестратор проставит
|
||||
@@ -202,6 +250,13 @@ class OnnxAsrBackend:
|
||||
"""Resolve alias to onnx-asr model name. Raw names pass through."""
|
||||
if model_name in MODEL_ALIASES:
|
||||
return MODEL_ALIASES[model_name]
|
||||
if model_name in _WHISPER_MODEL_NAMES:
|
||||
raise ValueError(
|
||||
f"Модель '{model_name}' относится к Whisper и не поддерживается "
|
||||
"ONNX-бэкендом. Без CUDA --device auto выбирает ONNX; "
|
||||
f"укажите --device openvino-cpu --model {model_name} "
|
||||
"или --device cuda --model medium."
|
||||
)
|
||||
if "/" in model_name or model_name.count("-") >= 2:
|
||||
# Looks like a raw onnx-asr name — allow passthrough
|
||||
return model_name
|
||||
@@ -211,6 +266,25 @@ class OnnxAsrBackend:
|
||||
f"Либо укажите полное имя модели onnx-asr."
|
||||
)
|
||||
|
||||
def _warn_if_language_unsupported(self, language: str | None) -> None:
|
||||
if (
|
||||
language is None
|
||||
or self._model_spec is None
|
||||
or language in self._model_spec.supported_languages
|
||||
):
|
||||
return
|
||||
|
||||
supported = ", ".join(sorted(self._model_spec.supported_languages))
|
||||
warnings.warn(
|
||||
f"Язык '{language}' не поддерживается моделью '{self._model_name}' "
|
||||
f"(поддерживаются: {supported}). Результат может быть некорректным. "
|
||||
"Для других языков используйте "
|
||||
"--device openvino-cpu --model medium "
|
||||
"или --device cuda --model medium.",
|
||||
UserWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
|
||||
def _format_compute_types(quantizations: frozenset[str | None]) -> str:
|
||||
values = [_compute_type_for_quantization(value) for value in quantizations]
|
||||
@@ -228,6 +302,12 @@ def _preferred_compute_type(quantizations: frozenset[str | None]) -> str:
|
||||
raise ValueError("Для ONNX-модели не указаны доступные квантизации")
|
||||
|
||||
|
||||
def _model_language(spec: OnnxModelSpec | None) -> str | None:
|
||||
if spec is not None and len(spec.supported_languages) == 1:
|
||||
return next(iter(spec.supported_languages))
|
||||
return None
|
||||
|
||||
|
||||
def _notify(on_status: Callable[[str], None] | None, message: str) -> None:
|
||||
if on_status is not None:
|
||||
on_status(message)
|
||||
|
||||
@@ -57,6 +57,19 @@ def _format_device_info(device_used: str) -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _format_language_mode(
|
||||
requested_language: str, result: TranscribeResult
|
||||
) -> str:
|
||||
"""Описывает источник языка, не выдавая профиль модели за детектор."""
|
||||
if requested_language != "auto":
|
||||
return "forced"
|
||||
if result.language_probability > 0:
|
||||
return "detected"
|
||||
if result.language not in {"", "auto", "unknown"}:
|
||||
return "из профиля модели"
|
||||
return "не определён"
|
||||
|
||||
|
||||
def _format_repetition_blocks(
|
||||
blocks: list[RepetitionBlock],
|
||||
use_hours: bool,
|
||||
@@ -315,7 +328,7 @@ def _run_single(
|
||||
)
|
||||
|
||||
device_info = _format_device_info(result.device_used)
|
||||
language_mode = "detected" if defaults["language"] == "auto" else "forced"
|
||||
language_mode = _format_language_mode(defaults["language"], result)
|
||||
|
||||
content = format_transcript(
|
||||
result=result,
|
||||
@@ -401,8 +414,6 @@ def _run_batch(
|
||||
# Phase 3: Transcribe
|
||||
processed = 0
|
||||
failed = 0
|
||||
language_mode = "detected" if defaults["language"] == "auto" else "forced"
|
||||
|
||||
batch_start = time.monotonic()
|
||||
|
||||
for i, file in enumerate(to_process, 1):
|
||||
@@ -442,6 +453,7 @@ def _run_batch(
|
||||
model_path = tfr.model_path
|
||||
|
||||
result = tfr.result
|
||||
language_mode = _format_language_mode(defaults["language"], result)
|
||||
|
||||
if len(result.segments) == 0:
|
||||
console.print(
|
||||
|
||||
@@ -80,7 +80,7 @@ def format_transcript(
|
||||
source_filename: str,
|
||||
model_name: str,
|
||||
device_info: str,
|
||||
language_mode: str, # "detected" | "forced"
|
||||
language_mode: str, # detected | forced | из профиля модели | не определён
|
||||
transcription_date: datetime | None = None, # None -> datetime.now()
|
||||
) -> str:
|
||||
"""Собирает markdown-транскрипт: шапка с метаданными + абзацы с таймкодами."""
|
||||
|
||||
Reference in New Issue
Block a user