From f966d4554bc38c0bee67092160d04d0a803248ff Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 29 Aug 2026 00:42:53 +0300 Subject: [PATCH 1/3] =?UTF-8?q?fix(deps):=20=D0=B2=D0=BE=D1=81=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D1=83=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=BD=D0=BE=D0=B2=D0=BA=D0=B0=20sherpa-onnx=20?= =?UTF-8?q?=D0=B2=20Linux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - в #27 чистая установка Linux не включала sherpa-onnx-core, поэтому import sherpa_onnx падал при загрузке libonnxruntime.so. - uv при универсальном резолвинге использовал метаданные armv7 wheel без Requires-Dist и терял обязательную платформенную зависимость. - Что: - версия sherpa-onnx зафиксирована на 1.13.5 вместе с явным metadata override для sherpa-onnx-core==1.13.5. - рядом с pin и override записано правило их совместного обновления. - uv.lock обновлен с зависимостью на core и wheel-файлами поддерживаемых платформ. - Проверка: - uv lock --check и uv tree --locked --package sherpa-onnx подтвердили зависимость от core 1.13.5. - чистые uv sync --frozen, uv pip check и import sherpa_onnx завершились успешно в Linux. - uv run pytest -q: 282 passed, 2 skipped. - uvx ruff check src/local_transcriber/speaker_diarizer.py tests/test_speaker_diarizer.py завершился успешно. --- pyproject.toml | 10 +++++++++- uv.lock | 28 +++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 716cdb9..50bef61 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,8 @@ dependencies = [ "faster-whisper>=1.2.1,<2", "httpx>=0.28,<1", "socksio>=1.0.0,<2", - "sherpa-onnx>=1.13.5,<2", + # При обновлении сверить блок tool.uv.dependency-metadata ниже. + "sherpa-onnx==1.13.5", "nvidia-cublas-cu12>=12.4,<13; sys_platform == 'linux' and platform_machine == 'x86_64'", "openvino-genai>=2026.3.0.0,<2026.4; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", "onnx-asr[cpu,hub]>=0.12,<0.13", @@ -34,3 +35,10 @@ testpaths = ["tests"] [tool.hatch.build.targets.wheel] packages = ["src/local_transcriber"] + +[[tool.uv.dependency-metadata]] +# Метаданные wheel-файлов sherpa расходятся, поэтому зависимость от core задана для uv явно. +# Версии sherpa, override и core меняются вместе; проверка: uv tree --locked --package sherpa-onnx +name = "sherpa-onnx" +version = "1.13.5" +requires-dist = ["sherpa-onnx-core==1.13.5"] diff --git a/uv.lock b/uv.lock index 36579d6..014a913 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,13 @@ version = 1 revision = 3 requires-python = ">=3.13" +[manifest] + +[[manifest.dependency-metadata]] +name = "sherpa-onnx" +version = "1.13.5" +requires-dist = ["sherpa-onnx-core==1.13.5"] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -284,7 +291,7 @@ requires-dist = [ { name = "onnxruntime", specifier = ">=1.28,<2" }, { name = "openvino-genai", marker = "(platform_machine == 'AMD64' and sys_platform != 'darwin') or (platform_machine == 'x86_64' and sys_platform != 'darwin')", specifier = ">=2026.3.0.0,<2026.4" }, { name = "rich", specifier = ">=14.3.3,<15" }, - { name = "sherpa-onnx", specifier = ">=1.13.5,<2" }, + { name = "sherpa-onnx", specifier = "==1.13.5" }, { name = "socksio", specifier = ">=1.0.0,<2" }, { name = "typer", specifier = ">=0.24.1,<1" }, ] @@ -601,6 +608,9 @@ wheels = [ name = "sherpa-onnx" version = "1.13.5" source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sherpa-onnx-core" }, +] sdist = { url = "https://files.pythonhosted.org/packages/6d/a1/8a7d8751bc71a0814f3b9332717909512a5ff919d07ed53e4baa861ef8c7/sherpa_onnx-1.13.5.tar.gz", hash = "sha256:14bebfe71365a2c678dd94cd08efa8e79df06318b17fe8e97b2e802a7881fd5a", size = 1037285, upload-time = "2026-08-11T08:17:31.817Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/61/6f/b70ae4c7e367e3d2dca99efc65152d22ad5d942619add19172d24267c14b/sherpa_onnx-1.13.5-cp313-cp313-linux_armv7l.whl", hash = "sha256:15a02d9d74143f336156cb8b4da826bc65c11daa28bd5c9099e9397b3660e7df", size = 11999210, upload-time = "2026-08-11T09:26:07.356Z" }, @@ -623,6 +633,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/5e/9a9c63e1dcd646c5f21df6d16e6770fedd956f8a54241f3382343c6289af/sherpa_onnx-1.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:a7e97d8457a51725ffa3fcb44cb1d57bfdb780bc390a9460a65e05bf3d2851b4", size = 2303336, upload-time = "2026-08-11T07:33:25.917Z" }, ] +[[package]] +name = "sherpa-onnx-core" +version = "1.13.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/fe/2a972bc05bc1c674275a97d16d387545c6efa811bf2a8dc1b6ac573f9fe1/sherpa_onnx_core-1.13.5-py3-none-macosx_10_15_universal2.whl", hash = "sha256:a5380f4f8ec609e1c68f2052e34a55862098fa0efa0bba0af33adbdc0231fc4c", size = 19850412, upload-time = "2026-08-11T07:15:17.913Z" }, + { url = "https://files.pythonhosted.org/packages/27/cd/5730372ce10ba08fda03a3e3dad4ec8bc2a62607530a8f32c44e63258c51/sherpa_onnx_core-1.13.5-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:6bb5c460cd432293a290c861fbd519e30ac290ca84b5acd1c4447c22aef03351", size = 10614601, upload-time = "2026-08-11T07:00:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/91/54/b753e2dbd2b0f09aed2d2082f35c41606af9712457278ff99d13eaae92d3/sherpa_onnx_core-1.13.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:899e88916efd96ee1eabc512e9ceaf2fdb711b20b2512cf40358b623e90b5ded", size = 9282744, upload-time = "2026-08-11T07:09:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e6/58/505668cedd7036bcba305c1a1f5424a3340f4701297a628de83d61e8ed58/sherpa_onnx_core-1.13.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4cd751063a378a49f0c72eba5ba959fe375397f5baf93a53f3db64097d00e2aa", size = 13051193, upload-time = "2026-08-11T07:00:47.526Z" }, + { url = "https://files.pythonhosted.org/packages/8f/61/2cf3aee64c280c830ca4fa25584eb87f8acf04787152744a29d576323977/sherpa_onnx_core-1.13.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:485b20188ddf905ffbd7504ed456a05d0ff84d10c592a16c0adb7d69bb845f82", size = 10371666, upload-time = "2026-08-11T07:19:12.791Z" }, + { url = "https://files.pythonhosted.org/packages/24/08/c04236fdf7825c4149b8b28b24d5a26f9e3fdca879aa24b8f1c65e96745b/sherpa_onnx_core-1.13.5-py3-none-manylinux_2_35_armv7l.whl", hash = "sha256:12697ff5745e225157b3257fe638e8b6305b1860366b6387e7a586ee6de2b18b", size = 9920766, upload-time = "2026-08-11T08:48:07.866Z" }, + { url = "https://files.pythonhosted.org/packages/44/c0/6d19a5b4be1541e2bd17ec045d0c48a641055d290007826ddee3a081a905/sherpa_onnx_core-1.13.5-py3-none-win32.whl", hash = "sha256:6b2f19b7559ad278aa17af0b9f3917a90a37fef4501c04afb14e722f4848c9b7", size = 14534505, upload-time = "2026-08-11T08:22:22.047Z" }, + { url = "https://files.pythonhosted.org/packages/2a/67/61611b5b945d7ac860387283a352860df8ab0431e3cc0868c1d5291e9f86/sherpa_onnx_core-1.13.5-py3-none-win_amd64.whl", hash = "sha256:fb48f5b7b4596bb745e5dcbb04616d841712224cba652717a71a4551d8ee5b63", size = 16522273, upload-time = "2026-08-11T07:09:46.3Z" }, + { url = "https://files.pythonhosted.org/packages/28/b2/d7ee13aba3fc40316e2363a5ac0780be7b0112050f7e8821bcd00d6af024/sherpa_onnx_core-1.13.5-py3-none-win_arm64.whl", hash = "sha256:7e24d3ddbcb1e6447467b214ff1235dced21fa4107de5c55125fb3c95ff10501", size = 15683505, upload-time = "2026-08-11T07:11:03.608Z" }, +] + [[package]] name = "socksio" version = "1.0.0" -- 2.54.0 From b412b73fc0b448f82b7bf0eef173c30a6e4985d7 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 29 Aug 2026 01:20:24 +0300 Subject: [PATCH 2/3] =?UTF-8?q?fix(cli):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=BF=D1=80=D0=B5=D0=B4=D1=83=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=B6=D0=B4=D0=B5=D0=BD=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - предупреждения из задачи #7 выглядели как фрагменты Python traceback. - Что: - на время запуска CLI установлен однострочный Rich-рендер предупреждений. - сохранены буквальный текст сообщений и восстановление глобального hook. - single- и batch-сценарии покрыты автоматическим тестом. - Проверка: - uv run pytest tests/test_cli.py -q. --- src/local_transcriber/cli.py | 27 ++++++++++++++++++++++ tests/test_cli.py | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 9305976..ef7f4de 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": @@ -193,6 +219,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..f1ab64d 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") -- 2.54.0 From d43c3b7c9a6faca50a260963421e48983136b539 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Sat, 29 Aug 2026 01:21:10 +0300 Subject: [PATCH 3/3] =?UTF-8?q?fix(cli):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=B0=D1=80=D0=BA=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B0=20ONNX=20=D0=B2=20=D1=88=D0=B0=D0=BF?= =?UTF-8?q?=D0=BA=D0=B5=20=D1=82=D1=80=D0=B0=D0=BD=D1=81=D0=BA=D1=80=D0=B8?= =?UTF-8?q?=D0=BF=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - транскрипты ONNX и faster-whisper на CPU имели одинаковую подпись. - Что: - для ONNX добавлено обозначение ONNX (CPU) в метаданных транскрипта. - различие движков и пользовательская шапка покрыты тестами. - примеры шапки в README и PRD приведены к текущему ONNX-профилю. - Проверка: - env -u FORCE_COLOR -u COLUMNS -u LINES uv run pytest. --- README.md | 4 ++-- docs/PRD.md | 6 +++--- src/local_transcriber/cli.py | 2 ++ tests/test_cli.py | 27 ++++++++++++++++++++++++++- 4 files changed, 33 insertions(+), 6 deletions(-) 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 ef7f4de..bc9c849 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -92,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" diff --git a/tests/test_cli.py b/tests/test_cli.py index f1ab64d..e56cd51 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -193,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") @@ -1424,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" -- 2.54.0