diff --git a/README.md b/README.md index a8c997c..3278ee2 100644 --- a/README.md +++ b/README.md @@ -413,10 +413,10 @@ device-aware дефолт недоступен для выбранной мод # Транскрипт: meeting.mp4 - **Дата транскрипции**: 2026-03-17 14:30:05 -- **Модель**: large-v3 +- **Модель**: gigaam-v3-e2e-rnnt - **Язык**: ru (задан явно) - **Длительность**: 01:23:45 -- **Устройство**: CUDA (NVIDIA GeForce RTX 3060) +- **Устройство**: ONNX (CPU) --- diff --git a/docs/PRD.md b/docs/PRD.md index b862a87..199f122 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -63,10 +63,10 @@ transcribe <путь_к_файлу> [опции] # Транскрипт: meeting-2026-03-17.mp4 - **Дата транскрипции**: 2026-03-17 14:30:05 -- **Модель**: large-v3 -- **Язык**: ru (задан явно) / ru (определён автоматически) +- **Модель**: gigaam-v3-e2e-rnnt +- **Язык**: ru (задан явно) - **Длительность**: 01:23:45 -- **Устройство**: CUDA (NVIDIA GeForce RTX 3060) +- **Устройство**: ONNX (CPU) --- diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 9305976..bc9c849 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -2,6 +2,9 @@ import sys import time +import warnings +from collections.abc import Callable +from functools import wraps from pathlib import Path import typer @@ -56,6 +59,29 @@ app = typer.Typer() console = Console(stderr=True) +def _show_cli_warning( + message: Warning | str, + *_args: object, + **_kwargs: object, +) -> None: + """Печатает предупреждение в пользовательском формате CLI.""" + console.print(f"Внимание: {message}", style="yellow", soft_wrap=True, markup=False) + + +def _with_cli_warning_renderer( + command: Callable[..., None], +) -> Callable[..., None]: + """Устанавливает CLI-рендер предупреждений на время одного запуска.""" + + @wraps(command) + def wrapped(*args: object, **kwargs: object) -> None: + with warnings.catch_warnings(): + warnings.showwarning = _show_cli_warning + command(*args, **kwargs) + + return wrapped + + def _format_device_info(device_used: str) -> str: """Формирует строку устройства для шапки транскрипта.""" if device_used == "cuda": @@ -66,6 +92,8 @@ def _format_device_info(device_used: str) -> str: 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" @@ -193,6 +221,7 @@ def _print_diarization_report( @app.command() +@_with_cli_warning_renderer def main( files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"), model: str | None = typer.Option( diff --git a/tests/test_cli.py b/tests/test_cli.py index a64aa33..e56cd51 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,4 @@ +import warnings from pathlib import Path from unittest.mock import MagicMock, patch @@ -118,6 +119,49 @@ def test_cli_happy_path_exit_code_zero(tmp_path): assert out.exit_code == 0 +@pytest.mark.parametrize("file_count", [1, 2]) +def test_cli_renders_runtime_warning_without_python_details(tmp_path, file_count): + files = [tmp_path / f"test-{index}.mp3" for index in range(file_count)] + for file in files: + file.write_bytes(b"fake") + + result = _make_result() + model = _make_model() + backend = _make_backend() + tfr = _make_tfr(result=result, model=model, backend=backend) + warning_message = ( + "Тестовое [bold]предупреждение[/bold] с длинным текстом, который " + "должен остаться одной логической строкой без служебных подробностей Python" + ) + + def transcribe_with_warning(**_kwargs): + warnings.warn(warning_message, stacklevel=2) + return tfr + + original_showwarning = warnings.showwarning + with ( + patch("local_transcriber.cli.load_config", return_value={}), + patch("local_transcriber.cli.validate_input_file", side_effect=lambda p: p), + patch("local_transcriber.cli.detect_device", return_value="cpu"), + patch( + "local_transcriber.cli.load_model", + return_value=(model, "cpu", backend, "/models/medium"), + ), + patch( + "local_transcriber.cli._transcribe_file", + side_effect=transcribe_with_warning, + ), + patch("local_transcriber.cli.write_transcript"), + ): + out = runner.invoke(app, [str(file) for file in files]) + + warning_lines = [line for line in out.output.splitlines() if "Тестовое" in line] + assert warning_lines == [f"Внимание: {warning_message}"] + assert "UserWarning" not in out.output + assert "warnings.warn" not in out.output + assert warnings.showwarning is original_showwarning + + def test_cli_default_options_passed_to_transcribe(tmp_path): audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") @@ -149,6 +193,30 @@ def test_cli_default_options_passed_to_transcribe(tmp_path): assert "Устройство: onnx" in out.output +def test_cli_identifies_onnx_backend_in_transcript_header(tmp_path): + audio = tmp_path / "test.mp3" + audio.write_bytes(b"fake") + result = _make_result(device_used="onnx") + model = _make_model() + backend = _make_backend() + tfr = _make_tfr(result=result, model=model, actual_device="onnx", backend=backend) + + with ( + patch("local_transcriber.cli.load_config", return_value={}), + patch("local_transcriber.cli.detect_device", return_value="onnx"), + patch( + "local_transcriber.cli.load_model", + return_value=(model, "onnx", backend, "/models/gigaam-v3-e2e-rnnt"), + ), + patch("local_transcriber.cli._transcribe_file", return_value=tfr), + ): + out = runner.invoke(app, [str(audio)]) + + content = (tmp_path / "test-transcript.md").read_text(encoding="utf-8") + assert out.exit_code == 0 + assert "- **Устройство**: ONNX (CPU)" in content + + def test_cli_custom_options(tmp_path): audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") @@ -1380,7 +1448,8 @@ def test_format_device_info_openvino_legacy(): assert _format_device_info("openvino") == "OpenVINO (CPU)" -def test_format_device_info_cpu(): +def test_format_device_info_distinguishes_onnx_from_faster_whisper_cpu(): + assert _format_device_info("onnx") == "ONNX (CPU)" assert _format_device_info("cpu") == "CPU"