From 2e69c2f33db1ea468f74a97557eda66d41ef7b11 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Wed, 12 Aug 2026 07:59:04 +0300 Subject: [PATCH 1/9] =?UTF-8?q?chore(openvino):=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D1=91=D0=BD=20=D1=81=D1=82=D0=B5=D0=BA=20=D0=B4?= =?UTF-8?q?=D0=BE=20=D0=BB=D0=B8=D0=BD=D0=B8=D0=B8=202026.3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужна поддерживаемая линия OpenVINO для новых моделей Whisper. - Что: - OpenVINO, GenAI и Tokenizers согласованно обновлены до 2026.3. - автоматический переход на следующую линию ограничен версией 2026.4. - Проверка: - uv run pytest. - реальный прогон medium INT8 на трёх русскоязычных записях. --- pyproject.toml | 2 +- uv.lock | 36 ++++++++++++++++++------------------ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6a3f7af..181b3a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "faster-whisper>=1.2.1,<2", "socksio>=1.0.0,<2", "nvidia-cublas-cu12>=12.4,<13; sys_platform == 'linux' and platform_machine == 'x86_64'", - "openvino-genai>=2026.0.0.0,<2026.1; sys_platform != 'darwin' and (platform_machine == 'x86_64' or platform_machine == 'AMD64')", + "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", "onnxruntime>=1.28,<2", ] diff --git a/uv.lock b/uv.lock index 9fe5dd1..1e60fd4 100644 --- a/uv.lock +++ b/uv.lock @@ -279,7 +279,7 @@ requires-dist = [ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=12.4,<13" }, { name = "onnx-asr", extras = ["cpu", "hub"], specifier = ">=0.12,<0.13" }, { 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.0.0.0,<2026.1" }, + { 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 = "socksio", specifier = ">=1.0.0,<2" }, { name = "typer", specifier = ">=0.24.1,<1" }, @@ -416,35 +416,35 @@ wheels = [ [[package]] name = "openvino" -version = "2026.0.0" +version = "2026.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, { name = "openvino-telemetry" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/71/3f/95b15760d7ae4b7dfefdbadeccae9604985d4335b221350e1bb04701a158/openvino-2026.0.0-20965-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:452902e4b8fba526e8740622cd5638c457d3ae44da7b39e6d4ef7919be0e95d4", size = 53444542, upload-time = "2026-02-23T16:10:46.343Z" }, - { url = "https://files.pythonhosted.org/packages/d4/26/82d326f3769c9e5c17d34ef10dd0aff4a94a3b550e1d2efe977e5754b84e/openvino-2026.0.0-20965-cp313-cp313-win_amd64.whl", hash = "sha256:14069e5af2ac8a77f6ea55d7020d34cc7d3538d49ebda91a18c00d4da830ab58", size = 69160606, upload-time = "2026-02-23T16:10:52.9Z" }, - { url = "https://files.pythonhosted.org/packages/fe/8e/52df01e8687f4711259729433226219c6839a0495988ddeb7e27af59aba8/openvino-2026.0.0-20965-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:79ce4b5d7f3a7f84de878752d97620b84e0c4a8ee550fb3bca9fc36fe5669450", size = 53449059, upload-time = "2026-02-23T16:10:59.54Z" }, - { url = "https://files.pythonhosted.org/packages/f7/a8/bc8e75d3f75ee2529a12be9522f52a8d0e5614f24c00f95a20b69f587d3c/openvino-2026.0.0-20965-cp314-cp314-win_amd64.whl", hash = "sha256:6d33440d290e67836825418134ecf9e17f150d50d3d9c07cf481997f5b1f0e6d", size = 69165824, upload-time = "2026-02-23T16:11:03.127Z" }, - { url = "https://files.pythonhosted.org/packages/a8/06/1a2caa07bfcb4e057048b8d5515a7aff35a2aa53ab5379762c1685cbeacc/openvino-2026.0.0-20965-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8cbe36e0dacd8676eb69c9a4b564fa430d784f6e2b0e18e7ebc363599256c184", size = 53502042, upload-time = "2026-02-23T16:11:09.744Z" }, - { url = "https://files.pythonhosted.org/packages/2c/dd/6a05a399d90ad4e8b15e0d5a4dd7de90c10484cb1585bd65e7e69e779762/openvino-2026.0.0-20965-cp314-cp314t-win_amd64.whl", hash = "sha256:a9e5524322c42f6a1283148fb70085aba8640a7d3067ede896085abbff5e33fe", size = 69325734, upload-time = "2026-02-23T16:11:13.283Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d3/69e7083339f32621359f0bce2be3a7454db3c9a71fa7492f0a0941c00037/openvino-2026.3.0-22451-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:b7d09f20f009b9167a863f615a2b27400ce025bda8086cc72a2eb6ac3bf1d2af", size = 57480757, upload-time = "2026-08-04T09:06:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/48/f8/f71508784562a3d427f52f59d74a6364d559e6e82bb112cab5d6a6a677bb/openvino-2026.3.0-22451-cp313-cp313-win_amd64.whl", hash = "sha256:1c41f2d1072d600c260741b350aaf4a09d53f821a9a8fc8989bdc79a46b50a2c", size = 75797507, upload-time = "2026-08-04T09:06:51.858Z" }, + { url = "https://files.pythonhosted.org/packages/d7/97/fdace942843da232ea06a5a67cc3a70d292873657dc933408fdb9bb796a8/openvino-2026.3.0-22451-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:81a64aebd1a80da93bc9413b3ba9c93846c7cac57161cd4d7b287b354d77ad19", size = 57482441, upload-time = "2026-08-04T09:06:59.277Z" }, + { url = "https://files.pythonhosted.org/packages/60/cb/ed637225c7373d9a4267e7fa7252c1f3767fbc9853a906d78068a279b73a/openvino-2026.3.0-22451-cp314-cp314-win_amd64.whl", hash = "sha256:07a8c1cb32e3f7942aab4a0c48404b591e63c89813906c7bc5b001f94bed447c", size = 75798958, upload-time = "2026-08-04T09:07:07.28Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3c/40c0bbd5c57fc0b5c0c41f9e6f0ec722f231ff25c37d20240d2baf446409/openvino-2026.3.0-22451-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:65f34103c28107e830e1fb70cb9c62afdb31cfde4f8b6056c942761796b1d4fe", size = 57526976, upload-time = "2026-08-04T09:07:15.131Z" }, + { url = "https://files.pythonhosted.org/packages/dc/ca/927354fa957dd0e8c8f53401dcd1239c4cd50d95a26ff5da3bc4b502f4dc/openvino-2026.3.0-22451-cp314-cp314t-win_amd64.whl", hash = "sha256:17581c5acbdaf9bf503cd21d5ad852df7e547073ad7a529661b19f40ab3c8db6", size = 75963071, upload-time = "2026-08-04T09:07:24.796Z" }, ] [[package]] name = "openvino-genai" -version = "2026.0.0.0" +version = "2026.3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openvino-tokenizers" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d3/98/fe5716b9dc2f29286b68b98a4c67bc33003e3b02cf4d92236345a2d7280e/openvino_genai-2026.0.0.0-2050-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:08f73852a455d63453e7f74f22b0aebd0c3579f8cb89133e66e438c09df26b71", size = 4930444, upload-time = "2026-02-23T16:17:33.953Z" }, - { url = "https://files.pythonhosted.org/packages/02/80/bc87545dfbbd7f4a3bd1667a4c9b5f1d8e1b476fec34225692bca6a86590/openvino_genai-2026.0.0.0-2050-cp313-cp313-win_amd64.whl", hash = "sha256:ada030b34a9e2fe0c6c406c2af2ce7b6e8cb16654329a469055fdac8a3eba19d", size = 2900293, upload-time = "2026-02-23T16:17:37.673Z" }, - { url = "https://files.pythonhosted.org/packages/af/09/18e678f2d0ac4ee993244d3cf0e6a80ec7013add2972153ad355d889d8cc/openvino_genai-2026.0.0.0-2050-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:77b73d1947d7f62a672857ef35d187074c6918de1db1d3f981452c2d67511c13", size = 4931196, upload-time = "2026-02-23T16:17:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/a4/be/3f02a70a16147b08b933908ef1c8af930ecd5c65ba78e14f988112d3d691/openvino_genai-2026.0.0.0-2050-cp314-cp314-win_amd64.whl", hash = "sha256:c59aeab5993c78194dc552cc9249bab509f7e44b1fb2f0d5a90c5e61869f06b8", size = 2900527, upload-time = "2026-02-23T16:17:43.244Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e8/7da6a1b86e3178e825560442ea4460ba4c92bb417fb8edda9e2210febec6/openvino_genai-2026.0.0.0-2050-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d807c5cabdea20bab2199ffa736831f57baaf0571e80d19cf4b65004f6b00747", size = 4943971, upload-time = "2026-02-23T16:17:46.021Z" }, - { url = "https://files.pythonhosted.org/packages/e3/19/221256ce3b0276bb3ba3ce7e8fc57b2968a662c2acde7427c342d917867f/openvino_genai-2026.0.0.0-2050-cp314-cp314t-win_amd64.whl", hash = "sha256:bb537f5a65203eee12326e6bb9578e834d96d94ab3fa48c806990ef04a455935", size = 2900592, upload-time = "2026-02-23T16:17:47.452Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/6968a1b95f6b9931741ef1a302c2e7ab2d3193f76224fead0ed736506db3/openvino_genai-2026.3.0.0-2495-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cd9f7bc035d2a23f01617e64f8963477335f15e048c6c9fcf1cce6c925fedf3e", size = 6211947, upload-time = "2026-08-04T09:34:59.252Z" }, + { url = "https://files.pythonhosted.org/packages/50/74/cec114195adf6237c000fabd6c5da61f04edf3b1d719db7601e452977e4f/openvino_genai-2026.3.0.0-2495-cp313-cp313-win_amd64.whl", hash = "sha256:08411eb51bd0bddc717e2d97c541cc5cc946bde70e74da6dd7428149e5e0cdbb", size = 3669271, upload-time = "2026-08-04T09:35:02.699Z" }, + { url = "https://files.pythonhosted.org/packages/ed/02/ed9f6773a40cc8b0fc166979abf6b56aed3a9f5840f9f0bf76fbf82cc349/openvino_genai-2026.3.0.0-2495-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:2df610ec970b66b95cc4987d888c543e979bda515aed0c8ffda99954d10b4133", size = 6213490, upload-time = "2026-08-04T09:35:05.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9d/9e877a92ff22e9cec0cb930bcccd58013050726af91a9c1b8ff15264ed27/openvino_genai-2026.3.0.0-2495-cp314-cp314-win_amd64.whl", hash = "sha256:9331df790dcf57d796b6486ce9d9219ad4172c851cd657fafb9c5c0c82177c43", size = 3670597, upload-time = "2026-08-04T09:35:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/72/98/a4747cc685a5d2cc252ccb337c022e455f5761041e0d19980d0884ecadd9/openvino_genai-2026.3.0.0-2495-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:58a9e7493391ce8478440fbc14610968255a06a2717068f476b1ba519fe488a1", size = 6222203, upload-time = "2026-08-04T09:35:11.83Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/af5aee755635dc1cd19bb7406410a099364205c3a35e49766e54ca174c15/openvino_genai-2026.3.0.0-2495-cp314-cp314t-win_amd64.whl", hash = "sha256:0d3770d721f336549747b3ce054ad6bc726609e1fc3f5ce90c4954c63f23e4b9", size = 3670880, upload-time = "2026-08-04T09:35:14.895Z" }, ] [[package]] @@ -458,14 +458,14 @@ wheels = [ [[package]] name = "openvino-tokenizers" -version = "2026.0.0.0" +version = "2026.3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "openvino" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/45/22/2b976c129d43b214034a16a999d54eb659d5b2f15c12b2be52ab44e3235d/openvino_tokenizers-2026.0.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:84ee496d9fe4ff7866ed9a1370cb71bf3af9526601a701c59d269854a2b0dfc7", size = 13693316, upload-time = "2026-02-23T16:12:13.41Z" }, - { url = "https://files.pythonhosted.org/packages/24/86/4e7428a2fb350311a7f616c897ee1427d70cd814a2d64b2af9efb81fb881/openvino_tokenizers-2026.0.0.0-py3-none-win_amd64.whl", hash = "sha256:90f77c203c40623733e867754b952e8f0b86d9c86782abdbe9cf4bf2c82ce693", size = 13988492, upload-time = "2026-02-23T16:12:19.586Z" }, + { url = "https://files.pythonhosted.org/packages/0b/68/c1ba2177f4ce1f455aae858548aece8b6491b862a6458b03c5d5dbf356ef/openvino_tokenizers-2026.3.0.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9d35bb52d353a30d1194cd21c43d3949d0fac853b5bd3721994f70acfea051e4", size = 1829895, upload-time = "2026-08-04T09:28:12.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/65/707874d377a245fbc5fbeaeac77e4aaf878f4e0938ab9d1203f8b01e5aab/openvino_tokenizers-2026.3.0.0-py3-none-win_amd64.whl", hash = "sha256:7c6fb2f1e9b6c3c4b2bdb992e69ef97869787d8486b193d155d24aaa9cd5fed0", size = 1541664, upload-time = "2026-08-04T09:28:15.307Z" }, ] [[package]] -- 2.54.0 From a65e5039f55958ebfb84236389da9743ffd45507 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Wed, 12 Aug 2026 08:00:46 +0300 Subject: [PATCH 2/9] =?UTF-8?q?feat(openvino):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D1=8C=20large-v3-turbo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужен быстрый качественный профиль Whisper для локального распознавания. - Что: - добавлены официальные варианты large-v3-turbo INT8 и FP16. - покрыты каталог, выбор квантизации, ошибка и создание модели. - Проверка: - uv run pytest. --- src/local_transcriber/backends/openvino.py | 2 + tests/test_backend_openvino.py | 55 ++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/local_transcriber/backends/openvino.py b/src/local_transcriber/backends/openvino.py index 0855d63..85b80bd 100644 --- a/src/local_transcriber/backends/openvino.py +++ b/src/local_transcriber/backends/openvino.py @@ -23,6 +23,8 @@ MODEL_REPOS: dict[tuple[str, str], str] = { ("medium", "fp16"): "OpenVINO/whisper-medium-fp16-ov", ("large-v3", "int8"): "OpenVINO/whisper-large-v3-int8-ov", ("large-v3", "fp16"): "OpenVINO/whisper-large-v3-fp16-ov", + ("large-v3-turbo", "int8"): "OpenVINO/whisper-large-v3-turbo-int8-ov", + ("large-v3-turbo", "fp16"): "OpenVINO/whisper-large-v3-turbo-fp16-ov", } # Fallback: если точная пара не найдена, пробуем альтернативный compute_type diff --git a/tests/test_backend_openvino.py b/tests/test_backend_openvino.py index df44470..79c0799 100644 --- a/tests/test_backend_openvino.py +++ b/tests/test_backend_openvino.py @@ -17,6 +17,20 @@ from local_transcriber.types import Segment # === _resolve_repo === +def test_model_catalog_contains_supported_profiles(): + assert MODEL_REPOS == { + ("tiny", "int8"): "OpenVINO/whisper-tiny-int8-ov", + ("base", "fp16"): "OpenVINO/whisper-base-fp16-ov", + ("small", "int8"): "OpenVINO/whisper-small-int8-ov", + ("medium", "int8"): "OpenVINO/whisper-medium-int8-ov", + ("medium", "fp16"): "OpenVINO/whisper-medium-fp16-ov", + ("large-v3", "int8"): "OpenVINO/whisper-large-v3-int8-ov", + ("large-v3", "fp16"): "OpenVINO/whisper-large-v3-fp16-ov", + ("large-v3-turbo", "int8"): "OpenVINO/whisper-large-v3-turbo-int8-ov", + ("large-v3-turbo", "fp16"): "OpenVINO/whisper-large-v3-turbo-fp16-ov", + } + + def test_resolve_repo_exact_match(): backend = OpenVINOBackend(compute_type_explicit=True) assert backend._resolve_repo("medium", "int8") == ("OpenVINO/whisper-medium-int8-ov", "int8") @@ -60,6 +74,26 @@ def test_resolve_repo_explicit_large_v3_int8_respected(): assert backend._resolve_repo("large-v3", "int8") == ("OpenVINO/whisper-large-v3-int8-ov", "int8") +@pytest.mark.parametrize("compute_type", ["int8", "fp16"]) +def test_resolve_repo_large_v3_turbo_quantization(compute_type): + backend = OpenVINOBackend(compute_type_explicit=True) + + assert backend._resolve_repo("large-v3-turbo", compute_type) == ( + f"OpenVINO/whisper-large-v3-turbo-{compute_type}-ov", + compute_type, + ) + + +def test_resolve_repo_large_v3_turbo_unsupported_quantization_raises(): + backend = OpenVINOBackend(compute_type_explicit=True) + + with pytest.raises( + ValueError, + match="Доступные варианты: fp16, int8", + ): + backend._resolve_repo("large-v3-turbo", "float32") + + # === ensure_model_available === @@ -101,6 +135,27 @@ def test_ensure_model_available_downloads(mock_download, tmp_path): assert any("Скачиваю" in s for s in statuses) +@patch("local_transcriber.backends.openvino.snapshot_download") +def test_large_v3_turbo_model_is_resolved_and_created(mock_download, tmp_path): + model_dir = tmp_path / "large-v3-turbo" + model_dir.mkdir() + (model_dir / "openvino_encoder_model.xml").write_text("") + (model_dir / "openvino_decoder_model.xml").write_text("") + mock_download.return_value = str(model_dir) + mock_ov = MagicMock() + + backend = OpenVINOBackend(ov_device="openvino-cpu", compute_type_explicit=True) + model_path = backend.ensure_model_available("large-v3-turbo", "int8") + with patch.dict("sys.modules", {"openvino_genai": mock_ov}): + backend.create_model(model_path, "openvino-cpu", "int8") + + mock_download.assert_called_once_with( + "OpenVINO/whisper-large-v3-turbo-int8-ov", + local_files_only=True, + ) + mock_ov.WhisperPipeline.assert_called_once_with(str(model_dir), "CPU") + + # === create_model === -- 2.54.0 From f043f4e3b55d5234f5768ea2d6c15154f68d7146 Mon Sep 17 00:00:00 2001 From: Dmitry Dementiev Date: Wed, 12 Aug 2026 08:46:46 +0300 Subject: [PATCH 3/9] =?UTF-8?q?docs(openvino):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D0=B0=D1=82=D1=8B=20=D1=81=D1=80=D0=B0=D0=B2=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20turbo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - нужна практическая оценка нового CPU-профиля на русских встречах. - Что: - зафиксированы скорость, WER, пунктуация, нагрузка и отзывчивость. - добавлены две независимые оценки качества и рекомендации пользователям. - Проверка: - uv run pytest. - реальные прогоны четырёх профилей на трёх записях. --- README.md | 12 +- ...8-12-openvino-large-v3-turbo-comparison.md | 188 ++++++++++++++++++ docs/gpu.md | 13 ++ 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md diff --git a/README.md b/README.md index 1e9cd3a..e555a7f 100644 --- a/README.md +++ b/README.md @@ -263,7 +263,8 @@ language = "en" - **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` - **Макс. скорость CPU (русский):** `--device onnx --model gigaam-v3` (17-29× RTF, без пунктуации; рекомендуется LLM-нормализация терминов после) -- **CPU с пунктуацией (русский):** `--device openvino-cpu --model medium` (5-6× RTF; для встреч ≤30 мин с равномерной громкостью — на длинных файлах с тихими фрагментами возможны галлюцинации) +- **Быстрый OpenVINO с низким WER:** `--device openvino-cpu --model large-v3-turbo --compute-type int8` (7,2× RTFx на контрольном Intel CPU; пунктуация может быть слабой) +- **OpenVINO для чтения и конспекта:** `--device openvino-cpu --model medium` (около 6× RTFx; независимая оценка показала лучшую сохранность содержания, чем turbo) - **Быстрый тест:** `tiny` — для проверки пайплайна
@@ -276,6 +277,7 @@ language = "en" | `small` | ~460 MB | ~1.5 GB | ★★★ | ★★★ | | `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ | | `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ | +| `large-v3-turbo` | ~1 GB | — | ★★★★ | ★★★★ | #### ONNX-модели (`--device onnx`) @@ -331,11 +333,15 @@ device-aware дефолт недоступен для выбранной мод `float16`/`fp16` и `float32` значительно стабильнее на записях >20 минут. > Для OpenVINO `--compute-type` выбирает предквантизированную модель (int8 или fp16), -> а не runtime-параметр. Для `large-v3` по умолчанию выбирается `fp16`. +> а не параметр времени выполнения. Для `large-v3` по умолчанию выбирается +> `fp16`; для `large-v3-turbo` доступны явные варианты `int8` и `fp16`, а +> неявный профиль OpenVINO использует `int8`.
-Подробнее: бенчмарки, OpenVINO, совместимость GPU, результаты тестирования — [docs/gpu.md](docs/gpu.md). +Подробнее: бенчмарки, OpenVINO, совместимость GPU, результаты тестирования — +[docs/gpu.md](docs/gpu.md). Сравнение `large-v3-turbo` с CPU-профилями: +[OpenVINO 2026.3 и large-v3-turbo](docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md).
Формат вывода diff --git a/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md new file mode 100644 index 0000000..5d37edc --- /dev/null +++ b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md @@ -0,0 +1,188 @@ +# Сравнение OpenVINO large-v3-turbo с CPU-профилями + +**Дата:** 2026-08-12 + +**Статус:** завершённое сравнение на трёх русскоязычных встречах + +## Цель + +Проверить обновление OpenVINO с линии 2026.0 до 2026.3 и определить, даёт ли +`large-v3-turbo` практическое преимущество перед `medium` и быстрым профилем +ONNX на CPU. Модель по умолчанию и порядок выбора `--device auto` в рамках +сравнения не менялись. + +## Оборудование и условия + +- ноутбук Lenovo 82JD; +- Intel Core i7-11800H, 8 ядер / 16 логических процессоров, 2,30 ГГц; +- 64 ГиБ DDR4-3200, два модуля Kingston по 32 ГиБ; +- Windows 11 Корпоративная, сборка 26200; +- схема питания «Сбалансированная»; +- OpenVINO 2026.3.0, OpenVINO GenAI и Tokenizers 2026.3.0.0; +- Python 3.13.13; +- язык во всех командах задан явно: `--language ru`; +- модели запускались последовательно, без конкурирующих прогонов. + +Кэш каждой модели предварительно прогревался. Время измерялось от запуска +команды до её завершения и включает чтение и декодирование локального файла, +но не скачивание модели. Загрузка процессора и свободная память опрашивались +раз в две секунды. Минимум свободной памяти характеризует всю систему, а не +только процесс распознавания. + +Во время прогонов система оставалась пригодной для обычной интерактивной +работы. На OpenVINO средняя загрузка составляла 54–62%. ONNX на двух записях +кратковременно занимал процессор полностью, но наблюдаемого зависания системы +не было. Это субъективное наблюдение, а не числовой порог приёмки. + +## Контрольный набор + +Использованы те же три записи общей длительностью 43:37 и те же внешние +транскрипты, что в [сравнении GigaAM и Whisper](2026-08-11-gigaam-model-comparison.md). +Размеры и SHA-256 всех шести локальных файлов совпали с опубликованным там +манифестом. + +Внешние транскрипты сами содержат ошибки, редактируют разговорную речь и +добавляют разделение по говорящим. Поэтому WER служит сравнительным сигналом +внутри одной записи, а не абсолютной оценкой качества. + +## Методика + +Команды OpenVINO имели следующий вид: + +```powershell +uv run transcribe ` + --device openvino-cpu ` + --model ` + --compute-type ` + --language ru ` + --force +``` + +Для ONNX использовались `--device onnx`, `--model gigaam-v3-e2e-rnnt` и +`--compute-type int8`. WER считался после удаления метаданных, таймкодов и +обозначений говорящих, приведения к нижнему регистру, замены `ё` на `е` и +удаления пунктуации. Итоговый WER — микроусреднение: сумма замен, удалений и +вставок делится на общее число слов во внешних текстах. Плотность пунктуации — +число знаков `.,!?…:;` на 1000 распознанных слов. + +## Проверка обновления OpenVINO + +До изменения зависимостей OpenVINO 2026.0 `medium` INT8 был повторно запущен +на всём наборе с прогретым кэшем. После обновления тот же профиль успешно +прошёл автоматические проверки и реальный прогон. + +| Стек и профиль | Суммарное время | RTFx | WER | Пунктуация / 1000 слов | Предупреждения | +|---|---:|---:|---:|---:|---:| +| OpenVINO 2026.0 medium INT8 | 417,0 с | 6,28× | 28,9% | 191 | 1 | +| OpenVINO 2026.3 medium INT8 | 436,7 с | 5,99× | 25,3% | 185 | 0 | + +На линии 2026.0 RSQM содержал 11 одинаковых последовательных сегментов и +получил предупреждение о возможной галлюцинации. После обновления повтор не +воспроизвёлся, а WER этой записи снизился с 34,2% до 23,3%. Суммарное время +выросло на 4,7%; из-за естественного разброса единичных запусков это не следует +считать устойчивой регрессией без серии повторов. + +## Результаты после обновления + +### Сводка по трём записям + +| Профиль | Время | RTFx | WER | Пунктуация / 1000 слов | Средний / пиковый CPU | Минимум свободной памяти | +|---|---:|---:|---:|---:|---:|---:| +| OpenVINO medium INT8 | 436,7 с | 5,99× | 25,3% | 185 | 54,3% / 85% | 36,25 ГиБ | +| **OpenVINO large-v3-turbo INT8** | **365,5 с** | **7,16×** | **24,0%** | 96 | 62,0% / 86% | 35,04 ГиБ | +| OpenVINO large-v3-turbo FP16 | 545,0 с | 4,80× | 24,8% | 103 | 57,1% / 87% | 32,96 ГиБ | +| ONNX GigaAM v3 E2E RNN-T INT8 | **265,7 с** | **9,85×** | 29,1% | **290** | 70,7% / 100% | 35,52 ГиБ | + +### По отдельным записям + +| Запись | Профиль | Время | RTFx | WER | Пунктуация / 1000 слов | +|---|---|---:|---:|---:|---:| +| BDMA | medium INT8 | 137,0 с | 6,50× | **23,0%** | 194 | +| BDMA | turbo INT8 | **118,7 с** | **7,51×** | 24,4% | 79 | +| BDMA | turbo FP16 | 180,0 с | 4,95× | 26,2% | 60 | +| BDMA | GigaAM RNN-T INT8 | **87,6 с** | **10,17×** | 29,0% | **326** | +| RSQM | medium INT8 | 125,7 с | 6,72× | 23,3% | 171 | +| RSQM | turbo INT8 | 116,9 с | 7,23× | **22,4%** | 4 | +| RSQM | turbo FP16 | 170,2 с | 4,96× | 23,3% | 31 | +| RSQM | GigaAM RNN-T INT8 | **92,9 с** | **9,09×** | 27,4% | **286** | +| DataCat | medium INT8 | 174,0 с | 5,07× | 28,1% | 188 | +| DataCat | turbo INT8 | **129,9 с** | **6,79×** | **24,7%** | 169 | +| DataCat | turbo FP16 | 194,7 с | 4,53× | **24,7%** | 181 | +| DataCat | GigaAM RNN-T INT8 | **85,2 с** | **10,36×** | 30,4% | **265** | + +## Качественная оценка + +Два независимых субагента читали полные транскрипты без таблицы численных +метрик. Один оценивал читаемость и пригодность для конспекта и протокола, +второй — сохранность содержания и терминов, пропуски, повторы и галлюцинации. + +### Читаемость и пригодность результата + +| Профиль | Читаемость | Для конспекта | Для протокола | +|---|---:|---:|---:| +| ONNX GigaAM RNN-T INT8 | **4/5** | **4/5** | **3/5** | +| OpenVINO medium INT8 | 3/5 | 3/5 | **3/5** | +| OpenVINO turbo INT8 | 2/5 | 3/5 | 2/5 | +| OpenVINO turbo FP16 | 2/5 | 3/5 | 2/5 | + +Проверяющий поставил GigaAM на первое место благодаря пунктуации, отделению +реплик и естественному ритму. Из текста без труда извлекаются решения: собрать +вопросы для архитектуры и сайзинга, не связываться с «дата-рентгеном» в пилоте, +уточнить способ доступа к FineBI. При этом протокол требует сверки терминов: +`XML` превращается в «Максмельная», `Spark` — в «парк», `MPP` — в «MP-шный». + +Medium в основном сохраняет ход мысли, но нестабилен в названиях: «бутылочка в +FineBI» вместо учётной записи, `NiFi` как `I5`, `edge cases` как `HKEYS`. +Оба turbo-профиля иногда лучше узнают отдельные имена и термины, включая +«Рома Иваницкий», `Open Lineage` и `DBC tables V`, однако длинные блоки почти +без пунктуации приходится разбирать вручную. Практического преимущества FP16 +над INT8 по читаемости не найдено. + +### Сохранность содержания + +| Профиль | Смысл | Термины | Пропуски | Повторы и выдумки | +|---|---:|---:|---:|---:| +| ONNX GigaAM RNN-T INT8 | **5/5** | **4/5** | **4/5** | **4/5** | +| OpenVINO medium INT8 | 4/5 | 3/5 | **4/5** | **4/5** | +| OpenVINO turbo FP16 | 4/5 | 3/5 | 3/5 | **4/5** | +| OpenVINO turbo INT8 | 3/5 | 3/5 | 3/5 | **4/5** | + +Второй проверяющий также поставил GigaAM первым: он лучше удерживает ход +рассуждения, отрицания и поручения. Среди OpenVINO medium оказался надёжнее +обоих turbo-профилей по сохранности содержания, несмотря на худший WER. + +Наиболее опасные искажения: + +- turbo FP16 в DataCat пропустил оговорку о том, что API-контракт извлечён из + JAR-файлов, а не документации; предположение выглядит установленным фактом; +- turbo INT8 почти потерял обсуждение доступа к DBC, риска перегрузить FineBI + через API и необходимости ограничить частоту запросов; +- medium превратил поручение посчитать объём и квоту Teradata в плохо читаемое + «нам требование это продать… конкретный объём в продате квоту»; +- GigaAM один раз заменил «МТС хочет продать дата-рентген» на «ты хочешь + продать дата-рентген», хотя соседняя фраза восстанавливает инициатора; +- ключ поиска `IMSI плюс время` не сохранился точно ни в одном профиле; +- у medium после фактического окончания RSQM появился одиночный сегмент «В». + +Длинных повторяющихся или полностью выдуманных фрагментов после обновления +проверяющие не нашли. Общий недостаток всех профилей — отсутствие надёжного +разделения по участникам, что ограничивает пригодность для формального протокола. + +## Вывод + +- `large-v3-turbo` реально работает с OpenVINO 2026.3 в вариантах INT8 и FP16. +- Turbo INT8 оказался на 16% быстрее medium INT8 и снизил суммарный WER с + 25,3% до 24,0%. Это лучший OpenVINO-профиль по численным показателям на + данном CPU, но не по независимой оценке сохранности содержания и читаемости. +- Turbo FP16 на этом CPU на 49% медленнее turbo INT8 и не дал выигрыша по WER. + Его стоит оставлять доступным для явного выбора и других устройств, но не + рекомендовать как основной CPU-профиль. +- GigaAM RNN-T остаётся самым быстрым и лучше всех расставляет пунктуацию. Он + уступает turbo по WER и сильнее загружает CPU, но оба независимых проверяющих + признали его лучшим для чтения и сохранения делового содержания. +- У turbo INT8 пунктуация нестабильна: на RSQM её почти нет. Для готового + протокола может потребоваться последующая расстановка пунктуации. +- Автоматических предупреждений о повторах или потере хвоста после обновления + не было; ручная проверка нашла у medium одиночный ложный сегмент в конце RSQM. +- Модель по умолчанию и порядок `--device auto` оставлены без изменений. Решение + об их изменении требует отдельной продуктовой задачи. diff --git a/docs/gpu.md b/docs/gpu.md index 1dcb67f..05810a7 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -28,6 +28,8 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM - **Модели**: предконвертированные из [HuggingFace](https://huggingface.co/OpenVINO) (int8/fp16) - **Дефолт**: `medium` + `int8` (для `large-v3` автоматически выбирается `fp16`) +- **Быстрый профиль с низким WER**: явный `large-v3-turbo` + `int8`; для + читаемости и сохранности содержания `medium` остаётся предпочтительнее - **Аудиодекодирование**: через PyAV (бандлит FFmpeg), системный ffmpeg не нужен ### Доступные OpenVINO модели @@ -39,9 +41,16 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM | small | OpenVINO/whisper-small-int8-ov | — | | medium | OpenVINO/whisper-medium-int8-ov | OpenVINO/whisper-medium-fp16-ov | | large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov | +| large-v3-turbo | OpenVINO/whisper-large-v3-turbo-int8-ov | OpenVINO/whisper-large-v3-turbo-fp16-ov | ### Результаты тестирования OpenVINO +Актуальное сравнение OpenVINO 2026.3 на трёх русскоязычных встречах: +[medium, large-v3-turbo и GigaAM](benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md). +На Intel Core i7-11800H `large-v3-turbo` INT8 обработал 43:37 аудио за +365,5 секунды (7,16× RTFx) при WER 24,0%. FP16 занял 545,0 секунды и получил +WER 24,8%, поэтому для CPU рекомендуется INT8. + Реальные записи рабочих созвонов (русский, техтермины: SQL, PostgreSQL, LDAP, DLP и др.). **Скорость (эталонный файл 16 мин, OpenVINO, medium int8):** @@ -76,6 +85,10 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM - **small** — для быстрого сканирования большого объёма видео по маске (`*.mp4`). Ошибки в отдельных словах; для обработки ИИ (МОМ, конспект) рискованно — "рецензия" вместо "лицензия" может исказить смысл. - **medium** — для повседневного использования и обработки ИИ. Ключевые термины верные, единичные ляпы не влияют на смысл конспекта. Оптимальный баланс скорости и качества. - **large-v3** — для важных записей, где нужна дословная точность. Лучшая пунктуация и связность. На OpenVINO (416с) быстрее, чем medium на чистом CPU (734с) — лучшее качество при выше скорости. +- **large-v3-turbo** — явный профиль для CPU с приоритетом скорости и низкого + WER. В проверенном наборе INT8 быстрее и численно точнее medium, но хуже по + независимой оценке читаемости и сохранности содержания; FP16 на CPU пользы + не показал. ## CPU бэкенд (CTranslate2 / faster-whisper) -- 2.54.0 From 12e17020bf88e8c8e54a35bd25921cdecb989761 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 10:33:44 +0300 Subject: [PATCH 4/9] =?UTF-8?q?docs(benchmark):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D0=B0=D1=82=D1=8B=20=D0=BF=D1=80=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=B0=20Ryzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - требуется подтвердить сравнение OpenVINO и ONNX на втором CPU и повторно проверить дубли medium. - Что: - добавлены метрики четырёх профилей на Ryzen 7 8845H. - зафиксированы четыре одинаковых прогона RSQM без повторов и результаты двух слепых ревью. - описаны аргументы для будущего выбора backend по умолчанию. - Проверка: - git diff --cached --check. --- ...8-12-openvino-large-v3-turbo-comparison.md | 163 +++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md index 5d37edc..6ce8b86 100644 --- a/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md +++ b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md @@ -2,7 +2,7 @@ **Дата:** 2026-08-12 -**Статус:** завершённое сравнение на трёх русскоязычных встречах +**Статус:** завершённое сравнение на трёх русскоязычных встречах и двух CPU ## Цель @@ -168,6 +168,160 @@ FineBI» вместо учётной записи, `NiFi` как `I5`, `edge cas проверяющие не нашли. Общий недостаток всех профилей — отсутствие надёжного разделения по участникам, что ограничивает пригодность для формального протокола. +## Повторный прогон на AMD Ryzen 7 8845H + +Повторный прогон выполнен на втором ноутбуке тем же коммитом, на тех же шести +файлах и с теми же параметрами. Размеры и SHA-256 всех файлов совпали с +манифестом. Результаты Intel выше не перезаписывались. + +### Оборудование и условия + +- ноутбук Lenovo 83D5; +- AMD Ryzen 7 8845H, 8 ядер / 16 логических процессоров; +- 32 ГиБ LPDDR5X, четыре чипа Micron, фактическая скорость 6400 MT/s; +- Windows 11 Корпоративная, сборка 26200; +- схема питания «Сбалансированная»; +- OpenVINO 2026.3.0, OpenVINO GenAI и Tokenizers 2026.3.0.0; +- Python 3.13.13; +- язык во всех командах задан явно: `--language ru`; +- кэш каждой модели предварительно прогрет, скачивание не включено во время; +- загрузка CPU и свободная память опрашивались раз в две секунды. + +Во время всех прогонов ноутбук оставался пригодным для интерактивной работы. +Минимум свободной памяти ниже, чем на Intel, из-за 32 ГиБ физической памяти и +фоновой нагрузки; это показатель всей системы, а не потребления одной модели. + +### Сводка по трём записям + +| Профиль | Время | RTFx | WER | Пунктуация / 1000 слов | Средний / пиковый CPU | Минимум свободной памяти | +|---|---:|---:|---:|---:|---:|---:| +| OpenVINO medium INT8 | 438,5 с | 5,97× | 22,9% | 193 | 59,2% / 89% | 6,65 ГиБ | +| **OpenVINO large-v3-turbo INT8** | **267,4 с** | **9,79×** | **19,6%** | 108 | 57,4% / 72% | 8,93 ГиБ | +| OpenVINO large-v3-turbo FP16 | 333,6 с | 7,85× | 20,1% | 114 | 59,0% / 78% | 8,39 ГиБ | +| **ONNX GigaAM v3 E2E RNN-T INT8** | **194,8 с** | **13,44×** | 26,9% | **290** | 56,2% / 82% | 11,71 ГиБ | + +Для `medium` на RSQM получились три измеряемых времени: 158,8, 118,3 и +129,3 секунды. Все три транскрипта и отдельный прогревочный проход содержательно +совпали. В таблице использована медиана 129,3 секунды, чтобы не выбирать +произвольно единичный фоновый выброс. + +### По отдельным записям + +| Запись | Профиль | Время | RTFx | WER | Пунктуация / 1000 слов | +|---|---|---:|---:|---:|---:| +| BDMA | medium INT8 | 140,3 с | 6,35× | 25,0% | 202 | +| BDMA | turbo INT8 | **88,8 с** | **10,03×** | 24,5% | 55 | +| BDMA | turbo FP16 | 109,1 с | 8,16× | **24,4%** | 61 | +| BDMA | GigaAM RNN-T INT8 | **67,5 с** | **13,21×** | 29,0% | **326** | +| RSQM | medium INT8 | 129,3 с | 6,53× | 20,2% | **175** | +| RSQM | turbo INT8 | **82,1 с** | **10,28×** | **16,7%** | 61 | +| RSQM | turbo FP16 | 104,6 с | 8,07× | 20,4% | 48 | +| RSQM | GigaAM RNN-T INT8 | **58,6 с** | **14,41×** | 25,4% | **286** | +| DataCat | medium INT8 | 168,9 с | 5,22× | 23,2% | **199** | +| DataCat | turbo INT8 | **96,5 с** | **9,14×** | 18,0% | 178 | +| DataCat | turbo FP16 | 119,8 с | 7,36× | **16,8%** | 197 | +| DataCat | GigaAM RNN-T INT8 | **68,7 с** | **12,83×** | 26,4% | **265** | + +### Повторы `medium` на паузах + +На линии 2026.0 один контрольный прогон RSQM на Intel содержал 11 одинаковых +последовательных сегментов. После обновления на Ryzen выполнены четыре прохода +того же файла: один прогревочный и три измеряемых. Во всех четырёх получены +одинаковые 152 сегмента, объединённые форматтером в 17 абзацев. Детектор серий +из четырёх и более одинаковых сегментов не сработал, различий в содержательной +части файлов нет, ложного хвоста также нет. + +На этом контрольном файле повтор после OpenVINO 2026.3 устойчиво не +воспроизводится на двух машинах. Это сильнее единичного успешного запуска, но +не доказывает устранение всего класса Whisper-галлюцинаций на тишине: набор +содержит только одну ранее проблемную запись. + +### Отличия Ryzen от Intel + +- `medium` показал практически одинаковое суммарное время: 438,5 против + 436,7 секунды. +- Turbo INT8 на Ryzen быстрее на 27%: 267,4 против 365,5 секунды; FP16 быстрее + на 39%: 333,6 против 545,0 секунды. +- GigaAM на Ryzen быстрее на 27%: 194,8 против 265,7 секунды. +- На Ryzen Turbo INT8 ускоряет обработку относительно `medium` на 39%, а + GigaAM — на 56%. +- Численные WER на Ryzen ниже у всех четырёх профилей. Поскольку файлы, + версии библиотек и заявленная методика совпадают, но сами выходы различаются. + Сравнение характеризует всю среду выполнения; приписывать разницу только + архитектуре CPU без отдельного исследования нельзя. +- Пунктуация Turbo остаётся нестабильной: на Ryzen она лучше результата Intel + для RSQM, но всё ещё значительно реже, чем у `medium` и GigaAM. + +### Независимая оценка качества на Ryzen + +Как и на первом ноутбуке, два субагента читали полные транскрипты вслепую, без +названий профилей и таблицы численных метрик. Их оценки разошлись по ожидаемой +причине: один ставил выше готовность текста к чтению, второй — точность +технических деталей. + +| Профиль | Читаемость | Для конспекта | Для протокола | +|---|---:|---:|---:| +| ONNX GigaAM RNN-T INT8 | **3/5** | **4/5** | **3/5** | +| OpenVINO medium INT8 | **3/5** | 3/5 | 2/5 | +| OpenVINO turbo FP16 | 2/5 | 3/5 | 2/5 | +| OpenVINO turbo INT8 | 2/5 | 2/5 | 2/5 | + +По читаемости GigaAM снова занял первое место: он лучше размечает смысловые +границы, сохраняет бытовые и организационные реплики и требует меньше ручной +чистки для чернового конспекта. `medium` оказался вторым. У обоих Turbo начало +нескольких записей превращается в длинные блоки нижнего регистра почти без +пунктуации. Ни один вариант не признан готовым формальным протоколом без сверки. + +| Профиль | Смысл | Термины | Полнота | Нет повторов и выдумок | +|---|---:|---:|---:|---:| +| OpenVINO turbo FP16 | **4/5** | **4/5** | 4/5 | **4/5** | +| OpenVINO turbo INT8 | 4/5 | **4/5** | 4/5 | **4/5** | +| OpenVINO medium INT8 | 4/5 | **4/5** | 4/5 | 3/5 | +| ONNX GigaAM RNN-T INT8 | 4/5 | 3/5 | **5/5** | 3/5 | + +По технической сохранности первым стал Turbo FP16, особенно на DataCat; Turbo +INT8 почти равен ему. Они лучше удерживают `FineBI`, `OpenLineage`, `Spark`, +`XML`, `DBC TablesV/ColumnsV`, `Open Platform` и архитектурные связи. GigaAM +самый полный, но чаще заменяет неразобранное гладко звучащими ложными терминами. + +Наиболее опасные места: + +- GigaAM потерял `IMSI` в требуемом ключе `IMSI плюс время`, исказил основание + оставить MPP-вычисления в Teradata и не сохранил Alation как технический + ориентир интеграции с FineBI; +- `medium` заменил Tele2 на `TeraData 2` и «админскую учётку» на «учётку от + Минска», то есть правдоподобно изменил адресата и источник доступа; +- Turbo INT8 почти потерял риск DDoS FineBI через API и необходимость + ограничивать частоту запросов; в другом месте ответ о production-базах + локально перевёрнут; +- Turbo FP16 хуже остальных передал `edge cases` на RSQM и потерял поручение + оценить требования и квоту Teradata, но лучше всего сохранил технически + насыщенный DataCat. + +Ни один проверяющий не нашёл длинных повторяющихся или автономно выдуманных +блоков. Основной остаточный риск всех профилей — локальная правдоподобная +подстановка термина, числа или адресата. + +### Что это означает для профиля по умолчанию + +Результаты поддерживают ONNX GigaAM RNN-T как основной кандидат для русской +речи на CPU: он быстрее всех на обеих машинах, дважды занял первое место по +читаемости и пригодности для конспекта и использует VAD. Но делать его +безусловным глобальным `--device auto` преждевременно: + +- GigaAM ориентирован на русский язык и хуже сохраняет латиницу, аббревиатуры, + названия систем и компаний; +- OpenVINO Whisper остаётся многоязычным путём и умеет использовать Intel GPU; +- Turbo лучше GigaAM по WER и техническим терминам, хотя хуже подготовлен для + непосредственного чтения; +- изменение существующего auto-пути будет несовместимым изменением поведения и + требует отдельной продуктовой задачи с language-aware выбором. + +Практичный кандидат на будущую политику: `CUDA` для NVIDIA, OpenVINO для Intel +GPU, ONNX GigaAM RNN-T для явно русской речи на CPU, OpenVINO/Whisper для других +языков и явный выбор Turbo для технически насыщенных встреч. В рамках текущей +задачи эта политика не реализуется. + ## Вывод - `large-v3-turbo` реально работает с OpenVINO 2026.3 в вариантах INT8 и FP16. @@ -186,3 +340,10 @@ FineBI» вместо учётной записи, `NiFi` как `I5`, `edge cas не было; ручная проверка нашла у medium одиночный ложный сегмент в конце RSQM. - Модель по умолчанию и порядок `--device auto` оставлены без изменений. Решение об их изменении требует отдельной продуктовой задачи. +- Повторный Ryzen-прогон подтвердил, что прежний блок из 11 повторов `medium` + не воспроизводится: четыре последовательных прохода дали одинаковый текст без + повторов и ложного хвоста. Это подтверждение для контрольного файла, а не + гарантия устранения всех Whisper-галлюцинаций на тишине. +- Для русской речи на CPU ONNX GigaAM RNN-T выглядит лучшим пользовательским + дефолтом по скорости и готовности текста к чтению. OpenVINO сохраняет сильные + аргументы для Intel GPU, других языков и технически насыщенных записей. -- 2.54.0 From 136e93765c6d8f4db8f6f6128aac83d37bedc11d Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 10:45:05 +0300 Subject: [PATCH 5/9] =?UTF-8?q?feat(auto):=20=D0=B2=D1=8B=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=20ONNX=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D1=8E=20=D0=B1=D0=B5=D0=B7=20CUDA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - пользователям без NVIDIA нужен самый быстрый и читаемый CPU-профиль без дополнительных параметров. - Что: - auto-политика изменена на CUDA при наличии nvidia-smi, иначе ONNX GigaAM RNN-T int8. - сохранён приоритет явных значений CLI и конфигурации для OpenVINO и FasterWhisper CPU. - обновлены тесты, README, PRD, ADR, GPU-документация и вывод benchmark. - Проверка: - uv run pytest -q: 232 passed, 1 skipped. - uv lock --check и git diff --cached --check. --- README.md | 36 +++++++---- docs/PRD.md | 64 ++++++++++--------- docs/adr/003-pluggable-backends.md | 15 +++-- docs/adr/006-onnx-asr-backend.md | 17 +++-- docs/backlog.md | 10 --- ...8-12-openvino-large-v3-turbo-comparison.md | 25 ++++---- docs/gpu.md | 13 ++-- src/local_transcriber/cli.py | 11 +++- src/local_transcriber/types.py | 2 +- src/local_transcriber/utils.py | 12 ++-- tests/test_cli.py | 58 +++++++++++++++-- tests/test_config.py | 10 ++- tests/test_utils.py | 19 +++--- 13 files changed, 177 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index e555a7f..54ce456 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ transcribe meeting.mp4 ``` - **Полностью локально** — данные не покидают машину -- **Авто-ускорение** — NVIDIA CUDA, Intel GPU (OpenVINO), ONNX (CPU), OpenVINO CPU или CPU fallback +- **Авто-ускорение** — NVIDIA CUDA при наличии GPU, иначе ONNX на CPU - **Батч-режим** — обработка нескольких файлов за один вызов - **Из проводника Windows** — пункт Transcribe в меню «Отправить» ([установка](#контекстное-меню-проводника-windows)) - **Markdown с таймкодами** — удобен для суммаризации ИИ @@ -33,18 +33,20 @@ uv tool install git+https://github.com/dementev-dev/local-transcriber **3. Ускорение (ставится автоматически):** -- **Intel GPU** (Arc, встроенная графика): работает через OpenVINO — ускорение в ~2x vs CPU -- **OpenVINO** (Intel/AMD x86 CPU): ставится автоматически на Linux и Windows — ускорение в 2-4x - **NVIDIA CUDA** (GPU): если есть GPU — транскрипция в 5-10× быстрее - **Windows**: `winget install -e --id Nvidia.CUDA --version 12.9` (от администратора), перезапустить терминал - **Linux / WSL2**: работает из коробки (нужен только драйвер: `nvidia-smi`) +- **Без NVIDIA GPU**: автоматически используется ONNX с GigaAM RNN-T на CPU +- **OpenVINO** для Intel GPU или x86 CPU остаётся доступен через явный + `--device openvino`, `--device openvino-gpu` или `--device openvino-cpu` **4. Готово:** ```bash transcribe meeting.mp4 ``` -Модели скачиваются автоматически при первом запуске (~1.5 GB для medium), нужен доступ в интернет. +Модели скачиваются автоматически при первом запуске; размер зависит от выбранного +профиля, нужен доступ в интернет.
transcribe: command not found @@ -103,7 +105,7 @@ HuggingFace Hub использует симлинки для экономии м ## Использование ```bash -# Простой запуск (medium, русский, автодетект устройства) +# Простой запуск (CUDA medium или ONNX GigaAM RNN-T, язык ru) transcribe meeting.mp4 # Указать язык @@ -180,11 +182,11 @@ transcribe --uninstall-menu | Опция | Сокращение | По умолчанию | Описание | |-------|-----------|-------------|----------| -| `--model` | `-m` | `medium` | Модель Whisper | +| `--model` | `-m` | medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX) | Модель распознавания | | `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) | | `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу | | `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) | -| `--compute-type` | — | float16 (CUDA) / int8 (OpenVINO/ONNX) / float32 (CPU) | Тип вычислений | +| `--compute-type` | — | float16 (CUDA) / int8 (ONNX/OpenVINO) / float32 (CPU) | Тип вычислений | | `--threads` | `-t` | 0 (авто) | Потоки CPU (рекомендуется = число физ. ядер) | | `--force` | `-f` | — | Перезаписать существующие транскрипты | | `--verbose` | `-v` | — | Подробный вывод | @@ -193,10 +195,9 @@ transcribe --uninstall-menu | | Linux / WSL2 | macOS | Windows | |---|---|---|---| -| CPU | ✅ | ✅ | ✅ | -| OpenVINO (x86 CPU) | ✅ авто | — | ✅ авто | -| OpenVINO (Intel GPU) | ✅ авто | — | ✅ авто | -| ONNX (CPU) | ✅ явно | ✅ явно | ✅ явно | +| CPU через ONNX | ✅ авто | ✅ авто | ✅ авто | +| OpenVINO (x86 CPU) | ✅ явно | — | ✅ явно | +| OpenVINO (Intel GPU) | ✅ явно | — | ✅ явно | | GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) |
@@ -237,8 +238,10 @@ transcribe --uninstall-menu Дефолтные параметры можно задать в `.transcriber.toml`: ```toml -model = "large-v3" -language = "en" +device = "openvino-cpu" +model = "large-v3-turbo" +compute_type = "int8" +language = "ru" ``` Порядок поиска: @@ -247,6 +250,11 @@ language = "en" Приоритет: **CLI-аргумент > конфиг > device-aware дефолт > встроенный дефолт**. +При `device = "auto"` выбирается CUDA, если доступен `nvidia-smi`, иначе ONNX. +Явный `device` из CLI или конфига отключает этот автоматический выбор. +Каталоги моделей различаются между бэкендами, поэтому при закреплении `model` +в конфиге рекомендуется явно закрепить и совместимый `device`. + Дефолты зависят от устройства: | Параметр | CUDA | OpenVINO (GPU) | OpenVINO (CPU) | ONNX | CPU | @@ -258,7 +266,7 @@ language = "en" ## Модели и GPU Рекомендации: -- **По умолчанию для ONNX:** `gigaam-v3-e2e-rnnt` — читаемый русский текст с +- **По умолчанию без CUDA:** ONNX `gigaam-v3-e2e-rnnt` — читаемый русский текст с пунктуацией почти без потери скорости относительно сырого `gigaam-v3` - **Макс. качество (NVIDIA):** `large-v3` + `--compute-type float16` - **Макс. качество (Intel GPU):** `large-v3` + `--device openvino-gpu` diff --git a/docs/PRD.md b/docs/PRD.md index f83bbc0..30e137a 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -24,8 +24,9 @@ transcribe meeting-2026-03-17.mp4 ### 3.1. Основной flow 1. Пользователь вызывает CLI, передаёт путь к файлу (или glob-маску, post-MVP) -2. Проверка: ffmpeg доступен в PATH -3. Файл передаётся в faster-whisper (он сам обрабатывает и аудио, и видео через libav/ffmpeg — отдельное извлечение аудиодорожки не нужно) +2. Файл валидируется по пути, размеру и расширению +3. По `device` выбирается backend; аудио декодируется через PyAV без отдельного + извлечения дорожки 4. Результат форматируется в markdown с таймкодами 5. Файл `<имя>-transcript.md` сохраняется рядом с исходным (кодировка: UTF-8) @@ -41,16 +42,16 @@ transcribe meeting-2026-03-17.mp4 transcribe <путь_к_файлу> [опции] Опции: - --model, -m Модель Whisper (tiny|base|small|medium|large-v3) - По умолчанию: large-v3 + --model, -m Модель распознавания + По умолчанию: medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX) --language, -l Язык (ru|en|auto) - По умолчанию: auto (автодетект) + По умолчанию: ru --output, -o Путь к выходному файлу По умолчанию: -transcript.md - --device, -d Устройство (auto|cpu|cuda|openvino|openvino-gpu|openvino-cpu) - По умолчанию: auto (CUDA → OpenVINO GPU → OpenVINO CPU → CPU) - --compute-type Тип вычислений (float16|int8|int8_float16|float32) - По умолчанию: int8 (универсален для GPU 4-8 GB и CPU) + --device, -d Устройство (auto|cpu|cuda|onnx|openvino|openvino-gpu|openvino-cpu) + По умолчанию: auto (CUDA при наличии, иначе ONNX CPU) + --compute-type Тип вычислений + По умолчанию: float16 (CUDA) / int8 (ONNX) --verbose, -v Подробный вывод (прогресс сегментов) ``` @@ -82,16 +83,17 @@ transcribe <путь_к_файлу> [опции] **Правила форматирования**: - Таймкоды в формате `[MM:SS.ss - MM:SS.ss]` (минуты:секунды.сотые) - Для записей длиннее 1 часа — `[HH:MM:SS.ss - HH:MM:SS.ss]` -- Каждый сегмент — отдельный абзац +- Соседние сегменты объединяются в абзац до паузы 2 секунды или длительности 60 секунд - Метаданные в шапке файла -- Пустая строка между сегментами для читаемости +- Пустая строка между абзацами для читаемости ### 3.4. Поддерживаемые форматы **Аудио**: mp3, wav, flac, ogg, m4a, wma, aac **Видео**: mp4, mkv, avi, mov, webm, ts -Определение типа — по расширению. Фактическое декодирование выполняет ffmpeg внутри faster-whisper; если формат не поддерживается, ошибка будет от ffmpeg. +Определение типа — по расширению. Фактическое декодирование выполняет PyAV с +встроенными библиотеками FFmpeg; системная установка `ffmpeg` не требуется. ## 4. Нефункциональные требования @@ -102,6 +104,8 @@ transcribe <путь_к_файлу> [опции] | RTX 3060 + large-v3 | ~10-15x (1 час аудио ≈ 4-6 мин) | | RTX 4050 + large-v3 | ~12-18x (1 час аудио ≈ 3-5 мин) | | Quadro M3000M + large-v3 | ~3-5x (1 час аудио ≈ 12-20 мин) | +| CPU + ONNX GigaAM RNN-T | ~10-14x (1 час аудио ≈ 4-6 мин) | +| CPU + OpenVINO Turbo INT8 | ~7-10x (1 час аудио ≈ 6-9 мин) | | CPU (modern) + large-v3 | ~0.5-1x (1 час аудио ≈ 60-120 мин) | | CPU + small | ~3-5x (1 час аудио ≈ 12-20 мин) | @@ -112,23 +116,22 @@ transcribe <путь_к_файлу> [опции] | RTX 3060 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | | RTX 4050 | 6 GB | ✅ | ✅ (впритык) | int8 — безопасный выбор | | Quadro M3000M | 4 GB | ✅ | ⚠️ может OOM | int8 обязательно | -| Без GPU | — | CPU int8 | — | int8 на CPU | +| Без GPU | — | ONNX GigaAM INT8 | — | auto выбирает ONNX | -Дефолт `int8` выбран как универсальный: работает на всех GPU от 4 GB и на CPU, при минимальной потере качества относительно float16. +Device-aware дефолты выбирают `float16` для CUDA и `int8` для ONNX/OpenVINO. ### 4.2. Требования к окружению - Python ≥ 3.13 -- ffmpeg в PATH (используется faster-whisper внутри для декодирования любых медиаформатов) - Для GPU: Linux/WSL2 — cuBLAS из nvidia-cublas-cu12 (ставится автоматически через `uv sync`); Windows — системный CUDA toolkit (см. ADR-001) -- Дисковое пространство для моделей: ~3 GB (large-v3) +- Дисковое пространство для моделей: зависит от выбранного backend и модели - Выходные файлы: UTF-8 (явная кодировка при записи) ### 4.3. Кроссплатформенность - Linux: нативный запуск - Windows: нативный Python или WSL2 -- macOS: не приоритет, но faster-whisper поддерживает CPU-режим +- macOS: ONNX CPU в auto-режиме; FasterWhisper CPU доступен явно ## 5. Технический стек @@ -136,19 +139,18 @@ transcribe <путь_к_файлу> [опции] |---------------------|-------------------------------------------------| | Язык | Python 3.13+ | | Управление проектом | uv (pyproject.toml) | -| Распознавание речи | faster-whisper (CTranslate2 backend) | -| Медиа-декодирование | ffmpeg (системная зависимость, используется faster-whisper внутри) | +| Распознавание речи | faster-whisper, ONNX Runtime, OpenVINO GenAI | +| Медиа-декодирование | PyAV со встроенными библиотеками FFmpeg | | CLI-фреймворк | typer | | Прогресс | rich (progress bar + статус) | -### 5.1. Почему faster-whisper +### 5.1. Почему несколько backend -- В 4× быстрее оригинального OpenAI Whisper при том же качестве -- Меньше потребление VRAM (large-v3 влезает в 6 GB с float16/int8) -- Нативный Python API, без Docker -- Поддержка CPU fallback из коробки -- Активное сообщество, регулярные обновления -- Автоматическая загрузка моделей из Hugging Face Hub +- faster-whisper оптимизирован для NVIDIA CUDA и поддерживает много языков +- ONNX GigaAM RNN-T даёт быстрый читаемый результат на CPU +- OpenVINO предоставляет явные профили для Intel GPU и x86 CPU +- Все backend работают локально через Python API, без Docker и облачных ключей +- Модели загружаются автоматически и кешируются локально ### 5.2. Структура проекта @@ -160,9 +162,10 @@ local-transcriber/ │ └── local_transcriber/ │ ├── __init__.py │ ├── cli.py # CLI entry point (typer) -│ ├── transcriber.py # Обёртка над faster-whisper +│ ├── transcriber.py # Оркестрация backend и fallback +│ ├── backends/ # Адаптеры FasterWhisper, ONNX и OpenVINO │ ├── formatter.py # Форматирование в markdown -│ └── utils.py # Проверки (ffmpeg), определение device и т.д. +│ └── utils.py # Проверки файлов и определение device └── tests/ └── ... ``` @@ -173,11 +176,10 @@ local-transcriber/ |------|---------|-----------| | Качество распознавания русского текста | Среднее | large-v3 хорошо справляется с ru; при проблемах — попробовать `--language ru` вместо auto | | Нет разделения по спикерам | Низкое | Осознанно выведено за скоуп MVP; добавление diarization (pyannote.audio) — возможное расширение | -| ffmpeg отсутствует в системе | Высокое | Проверка при старте + понятное сообщение об ошибке с инструкцией по установке | | Первый запуск: долгая загрузка модели | Низкое | Прогресс-бар при скачивании; модели кешируются в `~/.cache/huggingface/` | | CUDA несовместимость на Windows | Среднее | Автоматический fallback на CPU + предупреждение; в README — инструкция по CUDA | -| Большие файлы (>2 часов) | Низкое | faster-whisper работает потоково, не грузит всё в память | -| OOM на GPU с 4 GB VRAM | Среднее | Дефолт int8 (~2.5 GB); при OOM — fallback на CPU с предупреждением | +| Большие файлы (>2 часов) | Среднее | Учитывать память выбранного backend; чанкование рассматривается отдельно | +| OOM на GPU с 4 GB VRAM | Среднее | CUDA использует float16; при OOM — fallback на CPU с предупреждением или явный более лёгкий профиль | | Файл без речи (тишина, музыка, шум) | Низкое | Создаётся транскрипт с шапкой метаданных и `*Речь не обнаружена.*` в теле + предупреждение в stderr | ## 7. Вне скоупа MVP diff --git a/docs/adr/003-pluggable-backends.md b/docs/adr/003-pluggable-backends.md index ba812d4..7c99330 100644 --- a/docs/adr/003-pluggable-backends.md +++ b/docs/adr/003-pluggable-backends.md @@ -2,6 +2,7 @@ **Статус**: Принято **Дата**: 2026-03-21 +**Обновлено**: 2026-08-12 ## Контекст @@ -39,7 +40,8 @@ shared libraries. Ни то, ни другое не должно происхо Вместо отдельного `--backend` флага устройство само определяет бэкенд: - `cuda`, `cpu` → FasterWhisperBackend - `openvino` → OpenVINOBackend -- `auto` → CUDA (nvidia-smi) → OpenVINO (import check + x86) → CPU +- `onnx` → OnnxAsrBackend +- `auto` → CUDA при наличии `nvidia-smi`, иначе ONNX на CPU ### load_model() — единственный владелец pipeline @@ -71,18 +73,19 @@ OpenVINO модели предквантизированы (int8/fp16), compute_ - Из дефолтов: для large-v3 автоматически выбирается fp16 (стабильнее по качеству) - Несуществующая пара (model + compute_type) при явном выборе → ошибка -### Обе зависимости по умолчанию +### Зависимости бэкендов по умолчанию -faster-whisper (~37MB) и openvino-genai (~69MB) ставятся вместе — суммарно ~106MB, -приемлемо. Модели скачиваются только для активного бэкенда. CUDA (nvidia-cublas-cu12, -~554MB) остаётся conditional (Linux x86_64). OpenVINO — conditional (x86_64/AMD64, не macOS). +faster-whisper, onnx-asr/onnxruntime и openvino-genai ставятся вместе. Модели +скачиваются только для активного бэкенда. CUDA (`nvidia-cublas-cu12`) остаётся +conditional для Linux x86_64. OpenVINO — conditional для x86_64/AMD64, кроме +macOS; ONNX обеспечивает автоматический CPU-путь на остальных платформах. ## Последствия - Обратная совместимость: `transcribe()` сохранён; `load_model()` изменил сигнатуру (возвращает 4-tuple вместо 2-tuple, добавлен `compute_type_explicit`) - Новый бэкенд добавляется одним файлом в `backends/` + регистрацией в `__init__.py` - Модели скачиваются по запросу — CUDA пользователь не качает OpenVINO модели, и наоборот -- ARM и macOS: OpenVINO не ставится (platform markers), работает CPU через faster-whisper +- ARM и macOS: OpenVINO не ставится (platform markers), auto использует ONNX ## Отклонённые альтернативы diff --git a/docs/adr/006-onnx-asr-backend.md b/docs/adr/006-onnx-asr-backend.md index bec38b3..c52d1cd 100644 --- a/docs/adr/006-onnx-asr-backend.md +++ b/docs/adr/006-onnx-asr-backend.md @@ -2,7 +2,7 @@ **Статус**: Принято **Дата**: 2026-04-25 -**Обновлено**: 2026-08-11 +**Обновлено**: 2026-08-12 ## Контекст @@ -101,10 +101,11 @@ Mm-hmm-редукция и иностранные вставки **не обна ## Решение -**Принять onnx-asr как экспериментальный бэкенд с явным `--device onnx`. -GigaAM v3 E2E RNN-T — модель по умолчанию для русских встреч на CPU.** +**Принять onnx-asr как CPU-бэкенд автоматического профиля. +GigaAM v3 E2E RNN-T — модель по умолчанию на машинах без CUDA.** -Бэкенд **не в auto-detect** — только при явном указании пользователем (политика experimental backend, как для openvino). +Порядок `--device auto`: CUDA при наличии `nvidia-smi`, иначе ONNX. OpenVINO и +FasterWhisper CPU остаются доступными через явный CLI-аргумент или конфиг. Модели: - **`gigaam-v3-e2e-rnnt`** — модель по умолчанию: практически равна обычному @@ -122,7 +123,9 @@ GigaAM v3 E2E RNN-T — модель по умолчанию для русски - Пользователи CPU-only с русскоязычным контентом получают 3-5× ускорение по сравнению с OpenVINO medium **при превосходящем качестве** (4/5 vs 1-2/5 summary utility на длинных файлах). - Пользователи ONNX без явного `--model` получают пунктуацию и нормализацию RNN-T; более точный сырой CTC остаётся доступен как `--model gigaam-v3`. -- Whisper medium (`--device openvino-cpu`) **остаётся допустимым** для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с тихими участками он галлюцинирует целыми блоками — этот риск зафиксирован, но решение не выводит OpenVINO из списка дефолтов (часть пользователей всё ещё нуждается в пунктуации, и для коротких файлов галлюцинации не воспроизводятся). +- Whisper medium (`--device openvino-cpu`) **остаётся допустимым явным профилем** + для коротких (≤30 мин) встреч с равномерной громкостью; на длинных файлах с + тихими участками он может галлюцинировать целыми блоками. - Parakeet-v3 формально доступен, но в README рекомендуется только для англоязычного контента — для русского явно не годится. - GPU faster-whisper large-v3 остаётся эталоном по качеству (для пользователей с NVIDIA GPU). - Пост-процессинг GigaAM-транскрипта LLM-этапом нормализации (восстановление латинских терминов и имён компаний) — рекомендуемая практика для финального конспекта. @@ -131,7 +134,8 @@ GigaAM v3 E2E RNN-T — модель по умолчанию для русски - **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю. - **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация. -- **Auto-detect onnx**: после стабилизации в production-использовании (несколько недель) — рассмотреть включение в auto-detect как первый CPU-бэкенд (ниже CUDA, выше OpenVINO). +- Проверить профиль на других языках и при необходимости добавить явные + многоязычные рекомендации; автоматическая политика намеренно не зависит от языка. ## Отклонённые альтернативы @@ -139,5 +143,4 @@ GigaAM v3 E2E RNN-T — модель по умолчанию для русски |---|---| | NeMo Parakeet напрямую (без onnx-asr) | Требует PyTorch + CUDA, Python ≥ 3.12, ~2 GB зависимостей — слишком тяжело для CLI | | Замена faster-whisper на onnx-asr | faster-whisper поддерживает 99+ языков и пунктуацию, остаётся лучшим GPU-бэкендом | -| GigaAM как auto-detect default | Экспериментальный бэкенд, политика — не сюрпризить существующих пользователей; включение в auto-detect — после периода стабилизации | | Parakeet-v3 как multilingual default | Воспроизведённые проблемы из ADR-005 (Mm-hmm-редукция, иноязычные вставки) делают его непригодным для русского; для других языков не валидировано в этом эксперименте | diff --git a/docs/backlog.md b/docs/backlog.md index 35b9146..11090e9 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -252,16 +252,6 @@ SQL`), словарь пользовательский в `.transcriber.toml`. --- -### Включение `onnx` в `--device auto` - -**Что:** После периода стабилизации `--device onnx` (несколько недель production-использования без жалоб) — рассмотреть включение в auto-detect chain. - -**Порядок в chain (предложение):** CUDA → onnx (если CPU x86_64) → OpenVINO → CPU. - -**Почему откладывается:** политика experimental backend — не сюрпризить существующих пользователей до накопления опыта. Источник: [ADR-006](adr/006-onnx-asr-backend.md#решение). - ---- - ## Отклонённые направления *(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)* diff --git a/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md index 6ce8b86..b25cf5b 100644 --- a/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md +++ b/docs/benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md @@ -304,23 +304,21 @@ INT8 почти равен ему. Они лучше удерживают `FineB ### Что это означает для профиля по умолчанию -Результаты поддерживают ONNX GigaAM RNN-T как основной кандидат для русской -речи на CPU: он быстрее всех на обеих машинах, дважды занял первое место по -читаемости и пригодности для конспекта и использует VAD. Но делать его -безусловным глобальным `--device auto` преждевременно: +Результаты поддерживают ONNX GigaAM RNN-T как основной CPU-профиль: он быстрее +всех на обеих машинах, дважды занял первое место по читаемости и пригодности +для конспекта и использует VAD. После обсуждения принята простая политика: +`--device auto` выбирает CUDA при наличии `nvidia-smi`, иначе ONNX. + +Ограничения выбора сохраняются: - GigaAM ориентирован на русский язык и хуже сохраняет латиницу, аббревиатуры, названия систем и компаний; - OpenVINO Whisper остаётся многоязычным путём и умеет использовать Intel GPU; - Turbo лучше GigaAM по WER и техническим терминам, хотя хуже подготовлен для непосредственного чтения; -- изменение существующего auto-пути будет несовместимым изменением поведения и - требует отдельной продуктовой задачи с language-aware выбором. - -Практичный кандидат на будущую политику: `CUDA` для NVIDIA, OpenVINO для Intel -GPU, ONNX GigaAM RNN-T для явно русской речи на CPU, OpenVINO/Whisper для других -языков и явный выбор Turbo для технически насыщенных встреч. В рамках текущей -задачи эта политика не реализуется. +- изменение существующего auto-пути является несовместимым изменением поведения; + явные значения из CLI и конфигурации остаются способом сохранить прежний + OpenVINO/Whisper-профиль или выбрать Turbo для технически насыщенных встреч. ## Вывод @@ -338,8 +336,9 @@ GPU, ONNX GigaAM RNN-T для явно русской речи на CPU, OpenVIN протокола может потребоваться последующая расстановка пунктуации. - Автоматических предупреждений о повторах или потере хвоста после обновления не было; ручная проверка нашла у medium одиночный ложный сегмент в конце RSQM. -- Модель по умолчанию и порядок `--device auto` оставлены без изменений. Решение - об их изменении требует отдельной продуктовой задачи. +- В самом benchmark-коммите модель по умолчанию и порядок `--device auto` не + менялись; по итогам анализа отдельно принято решение использовать ONNX на + машинах без CUDA. - Повторный Ryzen-прогон подтвердил, что прежний блок из 11 повторов `medium` не воспроизводится: четыре последовательных прохода дали одинаковый текст без повторов и ложного хвоста. Это подтверждение для контрольного файла, а не diff --git a/docs/gpu.md b/docs/gpu.md index 05810a7..1fb94c0 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -2,7 +2,7 @@ ## Режимы `--device` -- `auto` (по умолчанию) — CUDA → OpenVINO GPU → OpenVINO CPU → CPU (первый доступный) +- `auto` (по умолчанию) — CUDA при наличии `nvidia-smi`, иначе ONNX на CPU - `cuda` — строго NVIDIA GPU, ошибка если недоступен - `openvino` — авто-выбор OpenVINO GPU или CPU - `openvino-gpu` — строго Intel GPU через OpenVINO @@ -14,12 +14,13 @@ | Оборудование | Рекомендуемый `--device` | Бэкенд | Ожидаемая скорость | |---|---|---|---| | NVIDIA GPU (6+ GB VRAM) | `auto` / `cuda` | faster-whisper (CTranslate2) | 7-19x реалтайм | -| Intel Arc iGPU / dGPU | `auto` / `openvino-gpu` | OpenVINO GenAI (GPU) | TBD | -| Intel/AMD x86 CPU | `auto` / `openvino-cpu` | OpenVINO GenAI (CPU) | 3-6x реалтайм* | -| Любой CPU (fallback) | `cpu` | faster-whisper (CTranslate2) | ~1.5x реалтайм | -| Apple Silicon (macOS) | `cpu` | faster-whisper (CTranslate2) | ~2x реалтайм | +| Любой CPU без NVIDIA | `auto` / `onnx` | ONNX GigaAM RNN-T | 10-14x реалтайм* | +| Intel Arc iGPU / dGPU | `openvino-gpu` | OpenVINO GenAI (GPU) | TBD | +| Intel/AMD x86 CPU | `openvino-cpu` | OpenVINO GenAI (CPU) | 3-10x реалтайм* | +| Любой CPU, FasterWhisper | `cpu` | faster-whisper (CTranslate2) | ~1.5x реалтайм | -\* По результатам тестирования на Intel и AMD CPU. Реальная скорость зависит от CPU и модели. +\* По результатам контрольных прогонов на Intel и AMD CPU. Реальная скорость +зависит от CPU, модели и записи. ## OpenVINO diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 62bc80e..65c041a 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -114,7 +114,11 @@ def _print_quality_warnings(result: TranscribeResult, file_name: str | None = No def main( files: list[Path] | None = typer.Argument(None, help="Пути к аудио/видеофайлам"), model: str | None = typer.Option( - None, "--model", "-m", show_default=False, help="Модель Whisper [по умолч.: medium]" + None, + "--model", + "-m", + show_default=False, + help="Модель [по умолч.: medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX)]", ), language: str | None = typer.Option( None, "--language", "-l", show_default=False, help="Язык [по умолч.: ru]" @@ -126,7 +130,10 @@ def main( ), compute_type: str | None = typer.Option( None, "--compute-type", show_default=False, - help="Тип вычислений [по умолч.: float16 (CUDA) / int8 (OpenVINO GPU/CPU) / float32 (CPU)]" + help=( + "Тип вычислений [по умолч.: float16 (CUDA) / " + "int8 (ONNX/OpenVINO) / float32 (CPU)]" + ), ), threads: int = typer.Option( 0, "--threads", "-t", show_default=False, min=0, diff --git a/src/local_transcriber/types.py b/src/local_transcriber/types.py index 295f7ba..f625dfb 100644 --- a/src/local_transcriber/types.py +++ b/src/local_transcriber/types.py @@ -18,7 +18,7 @@ class TranscribeResult: language: str language_probability: float duration: float # seconds - device_used: str # "cpu" / "cuda" / "openvino-gpu" / "openvino-cpu" + device_used: str # "cpu" / "cuda" / "onnx" / "openvino-gpu" / "openvino-cpu" @dataclass diff --git a/src/local_transcriber/utils.py b/src/local_transcriber/utils.py index d8d3360..8cfafe0 100644 --- a/src/local_transcriber/utils.py +++ b/src/local_transcriber/utils.py @@ -16,18 +16,16 @@ SUPPORTED_EXTENSIONS = { def detect_device(requested: str = "auto") -> str: """Определяет устройство для вычислений. - При ``requested="auto"`` проверяет: CUDA → OpenVINO GPU → OpenVINO CPU → CPU. - ``"openvino"`` — алиас для авто-детекта внутри OpenVINO (GPU если доступен, иначе CPU). + При ``requested="auto"`` выбирает CUDA при наличии ``nvidia-smi``, + иначе ONNX на CPU. + ``"openvino"`` — алиас для авто-детекта внутри OpenVINO + (GPU если доступен, иначе CPU). Возвращает всегда конкретное значение (не абстрактный ``"openvino"``). """ if requested == "auto": if shutil.which("nvidia-smi") is not None: return "cuda" - if _is_openvino_gpu_available(): - return "openvino-gpu" - if _is_openvino_available(): - return "openvino-cpu" - return "cpu" + return "onnx" if requested == "openvino": if _is_openvino_gpu_available(): return "openvino-gpu" diff --git a/tests/test_cli.py b/tests/test_cli.py index 4fae93a..040c06c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -73,27 +73,32 @@ def test_cli_happy_path_exit_code_zero(tmp_path): def test_cli_default_options_passed_to_transcribe(tmp_path): audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") - result = _make_result() + result = _make_result(device_used="onnx") model = _make_model() backend = _make_backend() - tfr = _make_tfr(result=result, model=model, backend=backend) + tfr = _make_tfr(result=result, model=model, actual_device="onnx", backend=backend) mock_transcribe_file = MagicMock(return_value=tfr) with ( patch("local_transcriber.cli.load_config", return_value={}), patch("local_transcriber.cli.validate_input_file", return_value=audio), - 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.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", mock_transcribe_file), patch("local_transcriber.cli.write_transcript"), ): - runner.invoke(app, [str(audio)]) + out = runner.invoke(app, [str(audio)]) call_kwargs = mock_transcribe_file.call_args[1] - assert call_kwargs["model_name"] == "medium" - assert call_kwargs["compute_type"] == "float32" + assert call_kwargs["model_name"] == "gigaam-v3-e2e-rnnt" + assert call_kwargs["compute_type"] == "int8" assert call_kwargs["language"] == "ru" assert call_kwargs["on_segment"] is None # verbose=False + assert "Модель: gigaam-v3-e2e-rnnt" in out.output + assert "Устройство: onnx" in out.output def test_cli_custom_options(tmp_path): @@ -660,6 +665,45 @@ def test_cli_config_applied(tmp_path): assert mock_load_model.call_args[0][0] == "tiny" +def test_cli_config_overrides_auto_device(tmp_path): + audio = tmp_path / "test.mp3" + audio.write_bytes(b"fake") + model = _make_model() + backend = _make_backend() + result = _make_result(device_used="openvino-cpu") + tfr = _make_tfr( + result=result, + model=model, + actual_device="openvino-cpu", + backend=backend, + ) + mock_detect_device = MagicMock(return_value="openvino-cpu") + mock_load_model = MagicMock( + return_value=(model, "openvino-cpu", backend, "/models/medium") + ) + + with ( + patch( + "local_transcriber.cli.load_config", + return_value={ + "device": "openvino-cpu", + "model": "medium", + "compute_type": "int8", + }, + ), + patch("local_transcriber.cli.validate_input_file", return_value=audio), + patch("local_transcriber.cli.detect_device", mock_detect_device), + patch("local_transcriber.cli.load_model", mock_load_model), + patch("local_transcriber.cli._transcribe_file", return_value=tfr), + patch("local_transcriber.cli.write_transcript"), + ): + out = runner.invoke(app, [str(audio)]) + + assert out.exit_code == 0 + assert mock_detect_device.call_args_list[0].args == ("openvino-cpu",) + assert mock_load_model.call_args.args[:3] == ("medium", "openvino-cpu", "int8") + + def test_cli_cli_overrides_config(tmp_path): audio = tmp_path / "test.mp3" audio.write_bytes(b"fake") diff --git a/tests/test_config.py b/tests/test_config.py index 00e834b..6e2162b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -71,11 +71,17 @@ def test_load_config_invalid_device(tmp_path): def test_resolve_defaults_cli_wins(): - config = {"model": "tiny", "language": "en"} - cli = {"model": "small", "language": None, "device": None, "compute_type": None} + config = {"model": "tiny", "language": "en", "device": "openvino-cpu"} + cli = { + "model": "small", + "language": None, + "device": "onnx", + "compute_type": None, + } result = resolve_defaults(cli, config) assert result["model"] == "small" assert result["language"] == "en" + assert result["device"] == "onnx" def test_resolve_defaults_config_wins(): diff --git a/tests/test_utils.py b/tests/test_utils.py index 8e2cb94..f95f089 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -61,27 +61,28 @@ def test_detect_device_explicit_passthrough(): """Явные device strings проходят без изменений.""" assert detect_device("cpu") == "cpu" assert detect_device("cuda") == "cuda" + assert detect_device("onnx") == "onnx" assert detect_device("openvino-gpu") == "openvino-gpu" assert detect_device("openvino-cpu") == "openvino-cpu" -def test_detect_device_auto_openvino_gpu(): - """auto + нет nvidia-smi + есть OpenVINO GPU → openvino-gpu.""" +def test_detect_device_auto_onnx_even_with_openvino_gpu(): + """auto + нет nvidia-smi → onnx, даже если доступен OpenVINO GPU.""" with ( patch("local_transcriber.utils.shutil.which", return_value=None), patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True), ): - assert detect_device("auto") == "openvino-gpu" + assert detect_device("auto") == "onnx" -def test_detect_device_auto_openvino_cpu(): - """auto + нет nvidia-smi + есть OpenVINO, нет GPU → openvino-cpu.""" +def test_detect_device_auto_onnx_even_with_openvino_cpu(): + """auto + нет nvidia-smi → onnx, даже если доступен OpenVINO CPU.""" with ( patch("local_transcriber.utils.shutil.which", return_value=None), patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False), patch("local_transcriber.utils._is_openvino_available", return_value=True), ): - assert detect_device("auto") == "openvino-cpu" + assert detect_device("auto") == "onnx" def test_detect_device_cuda_over_openvino(): @@ -93,14 +94,14 @@ def test_detect_device_cuda_over_openvino(): assert detect_device("auto") == "cuda" -def test_detect_device_auto_cpu_fallback(): - """Ни nvidia-smi, ни openvino → cpu.""" +def test_detect_device_auto_onnx_without_accelerators(): + """Без CUDA auto выбирает ONNX CPU.""" with ( patch("local_transcriber.utils.shutil.which", return_value=None), patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False), patch("local_transcriber.utils._is_openvino_available", return_value=False), ): - assert detect_device("auto") == "cpu" + assert detect_device("auto") == "onnx" def test_detect_device_openvino_resolves_to_gpu(): -- 2.54.0 From 995dbe61f8a01698489971daed6ff18ac71a2707 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 11:18:57 +0300 Subject: [PATCH 6/9] =?UTF-8?q?fix(auto)!:=20=D1=83=D1=87=D1=82=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20ONNX-=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - после смены 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 или конфигурацией. --- src/local_transcriber/backends/onnx_asr.py | 88 +++++++++++++++++++++- src/local_transcriber/cli.py | 18 ++++- src/local_transcriber/formatter.py | 2 +- tests/test_backend_openvino.py | 30 ++++---- tests/test_cli.py | 20 ++++- tests/test_onnx_asr.py | 84 +++++++++++++++++++++ tests/test_utils.py | 36 ++------- 7 files changed, 226 insertions(+), 52 deletions(-) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index d4469b0..ad9e0ee 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -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) diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 65c041a..3adebf1 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -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( diff --git a/src/local_transcriber/formatter.py b/src/local_transcriber/formatter.py index 9e0240a..55b0295 100644 --- a/src/local_transcriber/formatter.py +++ b/src/local_transcriber/formatter.py @@ -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-транскрипт: шапка с метаданными + абзацы с таймкодами.""" diff --git a/tests/test_backend_openvino.py b/tests/test_backend_openvino.py index 79c0799..5d7ad4a 100644 --- a/tests/test_backend_openvino.py +++ b/tests/test_backend_openvino.py @@ -17,15 +17,10 @@ from local_transcriber.types import Segment # === _resolve_repo === -def test_model_catalog_contains_supported_profiles(): - assert MODEL_REPOS == { - ("tiny", "int8"): "OpenVINO/whisper-tiny-int8-ov", - ("base", "fp16"): "OpenVINO/whisper-base-fp16-ov", - ("small", "int8"): "OpenVINO/whisper-small-int8-ov", - ("medium", "int8"): "OpenVINO/whisper-medium-int8-ov", - ("medium", "fp16"): "OpenVINO/whisper-medium-fp16-ov", - ("large-v3", "int8"): "OpenVINO/whisper-large-v3-int8-ov", - ("large-v3", "fp16"): "OpenVINO/whisper-large-v3-fp16-ov", +def test_model_catalog_contains_large_v3_turbo_profiles(): + assert { + pair: repo for pair, repo in MODEL_REPOS.items() if pair[0] == "large-v3-turbo" + } == { ("large-v3-turbo", "int8"): "OpenVINO/whisper-large-v3-turbo-int8-ov", ("large-v3-turbo", "fp16"): "OpenVINO/whisper-large-v3-turbo-fp16-ov", } @@ -61,11 +56,20 @@ def test_resolve_repo_implicit_fallback(): assert backend._resolve_repo("base", "int8") == ("OpenVINO/whisper-base-fp16-ov", "fp16") -def test_resolve_repo_implicit_large_v3_prefers_fp16(): - """Неявный compute_type: large-v3 автоматически получает fp16.""" +@pytest.mark.parametrize( + ("model_name", "expected_compute_type"), + [("large-v3", "fp16"), ("large-v3-turbo", "int8")], +) +def test_resolve_repo_implicit_large_v3_profiles( + model_name, expected_compute_type +): + """Неявный compute_type различает обычную и turbo-модель.""" backend = OpenVINOBackend(compute_type_explicit=False) - # Дефолт int8, но для large-v3 override на fp16 - assert backend._resolve_repo("large-v3", "int8") == ("OpenVINO/whisper-large-v3-fp16-ov", "fp16") + + assert backend._resolve_repo(model_name, "int8") == ( + f"OpenVINO/whisper-{model_name}-{expected_compute_type}-ov", + expected_compute_type, + ) def test_resolve_repo_explicit_large_v3_int8_respected(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 040c06c..1e5f3cd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,7 +5,7 @@ import pytest from rich.console import Console from typer.testing import CliRunner -from local_transcriber.cli import _format_device_info, app +from local_transcriber.cli import _format_device_info, _format_language_mode, app from local_transcriber.transcriber import Segment, TranscribeFileResult, TranscribeResult runner = CliRunner() @@ -42,6 +42,24 @@ def _make_tfr(result=None, model=None, actual_device="cpu", backend=None, model_ ) +@pytest.mark.parametrize( + ("requested_language", "language", "probability", "expected"), + [ + ("ru", "ru", 1.0, "forced"), + ("auto", "ru", 0.95, "detected"), + ("auto", "ru", 0.0, "из профиля модели"), + ("auto", "unknown", 0.0, "не определён"), + ], +) +def test_format_language_mode( + requested_language, language, probability, expected +): + result = _make_result(language=language) + result.language_probability = probability + + assert _format_language_mode(requested_language, result) == expected + + def _single_patches(result=None, tmp_file=None, actual_device="cpu"): """Patches for a standard single-file CLI happy path.""" if result is None: diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index dc64b45..2d1e611 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -1,5 +1,7 @@ """Tests for onnx-asr backend.""" +import warnings + import pytest from local_transcriber.backends.onnx_asr import OnnxAsrBackend @@ -224,6 +226,68 @@ class TestCreateModel: class TestTranscribe: + @pytest.mark.parametrize( + ("model_name", "language", "expects_warning"), + [ + ("gigaam-v3-e2e-rnnt", "en", True), + ("gigaam-v3-e2e-rnnt", "ru", False), + ("gigaam-multilingual-ctc", "en", False), + ], + ) + def test_warns_when_language_is_not_supported( + self, monkeypatch, tmp_path, model_name, language, expects_warning + ): + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + + monkeypatch.setattr( + "faster_whisper.decode_audio", + lambda path, sampling_rate=16000: [0.0] * 16000, + ) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + return iter(()) + + backend = OnnxAsrBackend() + backend.ensure_model_available(model_name, "int8") + + if expects_warning: + with pytest.warns( + UserWarning, + match=( + r"Язык 'en'.*--device openvino-cpu --model medium.*" + r"--device cuda --model medium" + ), + ): + backend.transcribe(FakeModel(), wav_file, language=language) + else: + with warnings.catch_warnings(record=True) as caught: + backend.transcribe(FakeModel(), wav_file, language=language) + assert caught == [] + + def test_auto_language_uses_single_supported_model_language( + self, monkeypatch, tmp_path + ): + wav_file = tmp_path / "test.wav" + wav_file.write_bytes(b"fake audio") + monkeypatch.setattr( + "faster_whisper.decode_audio", + lambda path, sampling_rate=16000: [0.0] * 16000, + ) + + class FakeModel: + def recognize(self, waveform, sample_rate, language=None): + return iter(()) + + backend = OnnxAsrBackend() + backend.ensure_model_available("gigaam-v3-e2e-rnnt", "int8") + + result = backend.transcribe(FakeModel(), wav_file, language=None) + + assert result.language == "ru" + assert result.language_probability == 0.0 + def test_transcribe_collects_segments(self, monkeypatch, tmp_path): """Verify transcribe maps VAD segments to project Segments.""" wav_file = tmp_path / "test.wav" @@ -386,3 +450,23 @@ class TestModelAliases: backend = OnnxAsrBackend() with pytest.raises(ValueError, match="Неподдерживаемая модель"): backend._resolve_model("nonexistent-model") + + def test_whisper_alias_error_suggests_explicit_backend(self): + backend = OnnxAsrBackend() + + with pytest.raises( + ValueError, + match=r"Whisper.*--device openvino-cpu.*--device cuda", + ): + backend._resolve_model("medium") + + def test_turbo_whisper_error_suggests_models_supported_by_backends(self): + backend = OnnxAsrBackend() + + with pytest.raises(ValueError) as exc_info: + backend._resolve_model("large-v3-turbo") + + message = str(exc_info.value) + assert "--device openvino-cpu --model large-v3-turbo" in message + assert "--device cuda --model medium" in message + assert "--device cuda --model large-v3-turbo" not in message diff --git a/tests/test_utils.py b/tests/test_utils.py index f95f089..0d919ea 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -66,41 +66,17 @@ def test_detect_device_explicit_passthrough(): assert detect_device("openvino-cpu") == "openvino-cpu" -def test_detect_device_auto_onnx_even_with_openvino_gpu(): - """auto + нет nvidia-smi → onnx, даже если доступен OpenVINO GPU.""" - with ( - patch("local_transcriber.utils.shutil.which", return_value=None), - patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True), - ): - assert detect_device("auto") == "onnx" - - -def test_detect_device_auto_onnx_even_with_openvino_cpu(): - """auto + нет nvidia-smi → onnx, даже если доступен OpenVINO CPU.""" - with ( - patch("local_transcriber.utils.shutil.which", return_value=None), - patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False), - patch("local_transcriber.utils._is_openvino_available", return_value=True), - ): - assert detect_device("auto") == "onnx" - - -def test_detect_device_cuda_over_openvino(): - """nvidia-smi доступен и openvino тоже → cuda побеждает.""" - with ( - patch("local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi"), - patch("local_transcriber.utils._is_openvino_gpu_available", return_value=True), +def test_detect_device_auto_cuda_when_nvidia_smi_available(): + """При доступном nvidia-smi auto выбирает CUDA.""" + with patch( + "local_transcriber.utils.shutil.which", return_value="/usr/bin/nvidia-smi" ): assert detect_device("auto") == "cuda" -def test_detect_device_auto_onnx_without_accelerators(): +def test_detect_device_auto_onnx_without_cuda(): """Без CUDA auto выбирает ONNX CPU.""" - with ( - patch("local_transcriber.utils.shutil.which", return_value=None), - patch("local_transcriber.utils._is_openvino_gpu_available", return_value=False), - patch("local_transcriber.utils._is_openvino_available", return_value=False), - ): + with patch("local_transcriber.utils.shutil.which", return_value=None): assert detect_device("auto") == "onnx" -- 2.54.0 From 9f11c329797ad300ac5dfe1d85760935ce3252bd Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 11:52:50 +0300 Subject: [PATCH 7/9] =?UTF-8?q?fix(cli)!:=20=D0=B8=D1=81=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=20=D1=8F=D0=B7=D1=8B=D0=BA=D0=B0=20=D0=B2=20?= =?UTF-8?q?=D1=88=D0=B0=D0=BF=D0=BA=D0=B5=20=D1=82=D1=80=D0=B0=D0=BD=D1=81?= =?UTF-8?q?=D0=BA=D1=80=D0=B8=D0=BF=D1=82=D0=B0=20=D0=BD=D0=B0=20=D1=80?= =?UTF-8?q?=D1=83=D1=81=D1=81=D0=BA=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - в одной строке шапки смешивались английские и русские значения, а при неизвестном языке в неё попадало служебное «unknown (не определён)». - Что: - формулировки источника языка вынесены константами в formatter и переведены на русский. - оба бэкенда используют общий признак UNKNOWN_LANGUAGE вместо «auto» и «unknown». - неизвестный язык печатается одной строкой, без служебного значения. - Проверка: - uv run pytest -q: 242 passed, 1 skipped. - ruff check .: 48 замечаний, столько же, сколько до правки. BREAKING CHANGE: в шапке транскрипта значения detected и forced заменены на «определён автоматически» и «задан явно»; при неизвестном языке строка выглядит как «- **Язык**: не определён». --- src/local_transcriber/backends/onnx_asr.py | 6 ++- src/local_transcriber/backends/openvino.py | 4 +- src/local_transcriber/cli.py | 15 ++++--- src/local_transcriber/formatter.py | 20 ++++++++- src/local_transcriber/types.py | 5 ++- tests/test_backend_openvino.py | 4 +- tests/test_cli.py | 15 +++++-- tests/test_formatter.py | 49 ++++++++++++++++------ tests/test_onnx_asr.py | 4 +- 9 files changed, 90 insertions(+), 32 deletions(-) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index ad9e0ee..5946ea1 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -8,7 +8,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Any -from local_transcriber.types import Segment, TranscribeResult +from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult @dataclass(frozen=True) @@ -216,7 +216,9 @@ class OnnxAsrBackend: _notify(on_status, "Транскрибирую (onnx-asr)...") segments: list[Segment] = [] - result_language = language or _model_language(self._model_spec) or "unknown" + result_language = ( + language or _model_language(self._model_spec) or UNKNOWN_LANGUAGE + ) for vad_seg in model.recognize( audio_array, sample_rate=16000, language=language diff --git a/src/local_transcriber/backends/openvino.py b/src/local_transcriber/backends/openvino.py index 85b80bd..2fc618a 100644 --- a/src/local_transcriber/backends/openvino.py +++ b/src/local_transcriber/backends/openvino.py @@ -12,7 +12,7 @@ from typing import Any from huggingface_hub import snapshot_download from huggingface_hub.errors import LocalEntryNotFoundError -from local_transcriber.types import Segment, TranscribeResult +from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult # (model_alias, compute_type) → HF repo MODEL_REPOS: dict[tuple[str, str], str] = { @@ -159,7 +159,7 @@ class OpenVINOBackend: f"Транскрибирую (OpenVINO)... [{len(segments)} сегм.]", ) - detected_language = language or "auto" + detected_language = language or UNKNOWN_LANGUAGE language_probability = 1.0 if language else 0.0 return TranscribeResult( diff --git a/src/local_transcriber/cli.py b/src/local_transcriber/cli.py index 3adebf1..7698908 100644 --- a/src/local_transcriber/cli.py +++ b/src/local_transcriber/cli.py @@ -12,6 +12,10 @@ from .config import apply_device_defaults, load_config, resolve_defaults from .context_menu import install_menu as install_context_menu from .context_menu import uninstall_menu as uninstall_context_menu from .formatter import ( + LANGUAGE_DETECTED, + LANGUAGE_FORCED, + LANGUAGE_FROM_MODEL, + LANGUAGE_UNKNOWN, format_duration, format_timestamp, format_transcript, @@ -30,6 +34,7 @@ from .transcriber import ( _transcribe_file, load_model, ) +from .types import UNKNOWN_LANGUAGE from .utils import ( build_output_path, detect_device, @@ -62,12 +67,12 @@ def _format_language_mode( ) -> str: """Описывает источник языка, не выдавая профиль модели за детектор.""" if requested_language != "auto": - return "forced" + return LANGUAGE_FORCED if result.language_probability > 0: - return "detected" - if result.language not in {"", "auto", "unknown"}: - return "из профиля модели" - return "не определён" + return LANGUAGE_DETECTED + if result.language not in {"", UNKNOWN_LANGUAGE}: + return LANGUAGE_FROM_MODEL + return LANGUAGE_UNKNOWN def _format_repetition_blocks( diff --git a/src/local_transcriber/formatter.py b/src/local_transcriber/formatter.py index 55b0295..dc4a2e3 100644 --- a/src/local_transcriber/formatter.py +++ b/src/local_transcriber/formatter.py @@ -10,6 +10,19 @@ from .types import Segment, TranscribeResult _PAUSE_THRESHOLD_S = 2.0 # пауза между сегментами для разбиения на абзацы _MAX_PARAGRAPH_S = 60.0 # максимальная длительность абзаца +# Источник языка в шапке транскрипта +LANGUAGE_FORCED = "задан явно" +LANGUAGE_DETECTED = "определён автоматически" +LANGUAGE_FROM_MODEL = "из профиля модели" +LANGUAGE_UNKNOWN = "не определён" + +LANGUAGE_MODES = ( + LANGUAGE_FORCED, + LANGUAGE_DETECTED, + LANGUAGE_FROM_MODEL, + LANGUAGE_UNKNOWN, +) + @dataclass class _Paragraph: @@ -80,7 +93,7 @@ def format_transcript( source_filename: str, model_name: str, device_info: str, - language_mode: str, # detected | forced | из профиля модели | не определён + language_mode: str, # см. LANGUAGE_MODES transcription_date: datetime | None = None, # None -> datetime.now() ) -> str: """Собирает markdown-транскрипт: шапка с метаданными + абзацы с таймкодами.""" @@ -92,7 +105,10 @@ def format_transcript( lines.append("") lines.append(f"- **Дата транскрипции**: {date.strftime('%Y-%m-%d %H:%M:%S')}") lines.append(f"- **Модель**: {model_name}") - lines.append(f"- **Язык**: {result.language} ({language_mode})") + if language_mode == LANGUAGE_UNKNOWN: + lines.append(f"- **Язык**: {LANGUAGE_UNKNOWN}") + else: + lines.append(f"- **Язык**: {result.language} ({language_mode})") lines.append(f"- **Длительность**: {format_duration(result.duration)}") if tail_gap(result) > TAIL_GAP_WARN_S: last_end = result.segments[-1].end diff --git a/src/local_transcriber/types.py b/src/local_transcriber/types.py index f625dfb..1dc84cb 100644 --- a/src/local_transcriber/types.py +++ b/src/local_transcriber/types.py @@ -4,6 +4,9 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any +# Единый признак «язык неизвестен» для всех бэкендов +UNKNOWN_LANGUAGE = "unknown" + @dataclass class Segment: @@ -15,7 +18,7 @@ class Segment: @dataclass class TranscribeResult: segments: list[Segment] - language: str + language: str # код языка или UNKNOWN_LANGUAGE, если он неизвестен language_probability: float duration: float # seconds device_used: str # "cpu" / "cuda" / "onnx" / "openvino-gpu" / "openvino-cpu" diff --git a/tests/test_backend_openvino.py b/tests/test_backend_openvino.py index 5d7ad4a..361bb6d 100644 --- a/tests/test_backend_openvino.py +++ b/tests/test_backend_openvino.py @@ -11,7 +11,7 @@ from local_transcriber.backends.openvino import ( OpenVINOBackend, _validate_model_dir, ) -from local_transcriber.types import Segment +from local_transcriber.types import UNKNOWN_LANGUAGE, Segment # === _resolve_repo === @@ -301,7 +301,7 @@ def test_transcribe_no_language_auto(): call_kwargs = mock_model.generate.call_args.kwargs assert "language" not in call_kwargs - assert result.language == "auto" + assert result.language == UNKNOWN_LANGUAGE assert result.language_probability == 0.0 diff --git a/tests/test_cli.py b/tests/test_cli.py index 1e5f3cd..f40e2e0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -6,7 +6,14 @@ from rich.console import Console from typer.testing import CliRunner from local_transcriber.cli import _format_device_info, _format_language_mode, app +from local_transcriber.formatter import ( + LANGUAGE_DETECTED, + LANGUAGE_FORCED, + LANGUAGE_FROM_MODEL, + LANGUAGE_UNKNOWN, +) from local_transcriber.transcriber import Segment, TranscribeFileResult, TranscribeResult +from local_transcriber.types import UNKNOWN_LANGUAGE runner = CliRunner() @@ -45,10 +52,10 @@ def _make_tfr(result=None, model=None, actual_device="cpu", backend=None, model_ @pytest.mark.parametrize( ("requested_language", "language", "probability", "expected"), [ - ("ru", "ru", 1.0, "forced"), - ("auto", "ru", 0.95, "detected"), - ("auto", "ru", 0.0, "из профиля модели"), - ("auto", "unknown", 0.0, "не определён"), + ("ru", "ru", 1.0, LANGUAGE_FORCED), + ("auto", "ru", 0.95, LANGUAGE_DETECTED), + ("auto", "ru", 0.0, LANGUAGE_FROM_MODEL), + ("auto", UNKNOWN_LANGUAGE, 0.0, LANGUAGE_UNKNOWN), ], ) def test_format_language_mode( diff --git a/tests/test_formatter.py b/tests/test_formatter.py index 28bf201..ac1e3d5 100644 --- a/tests/test_formatter.py +++ b/tests/test_formatter.py @@ -2,12 +2,16 @@ from datetime import datetime from pathlib import Path from local_transcriber.formatter import ( + LANGUAGE_DETECTED, + LANGUAGE_FORCED, + LANGUAGE_UNKNOWN, _group_segments, format_timestamp, format_transcript, write_transcript, ) from local_transcriber.transcriber import Segment, TranscribeResult +from local_transcriber.types import UNKNOWN_LANGUAGE def test_format_timestamp_minutes(): @@ -40,14 +44,14 @@ def test_format_transcript_basic(): source_filename="meeting.mp4", model_name="large-v3", device_info="CUDA (NVIDIA GeForce RTX 3060)", - language_mode="detected", + language_mode=LANGUAGE_DETECTED, transcription_date=datetime(2026, 3, 17, 14, 30, 5), ) assert "# Транскрипт: meeting.mp4" in content assert "**Дата транскрипции**: 2026-03-17 14:30:05" in content assert "**Модель**: large-v3" in content - assert "**Язык**: ru (detected)" in content + assert "**Язык**: ru (определён автоматически)" in content assert "**Длительность**: 02:00" in content assert "**Устройство**: CUDA (NVIDIA GeForce RTX 3060)" in content assert "---" in content @@ -55,6 +59,27 @@ def test_format_transcript_basic(): assert "[00:00.00 - 00:09.15] Добрый день, коллеги. Первый вопрос." in content +def test_format_transcript_unknown_language_without_placeholder(): + """Неизвестный язык печатается одной строкой, без служебного значения.""" + result = TranscribeResult( + segments=[Segment(start=0.0, end=4.0, text=" Добрый день.")], + language=UNKNOWN_LANGUAGE, + language_probability=0.0, + duration=120.0, + device_used="openvino-cpu", + ) + content = format_transcript( + result, + source_filename="meeting.mp4", + model_name="medium", + device_info="OpenVINO (CPU)", + language_mode=LANGUAGE_UNKNOWN, + ) + + assert "**Язык**: не определён" in content + assert UNKNOWN_LANGUAGE not in content + + def test_format_transcript_segment_no_leading_space(): """Сегменты без ведущего пробела должны форматироваться корректно.""" result = TranscribeResult( @@ -69,7 +94,7 @@ def test_format_transcript_segment_no_leading_space(): source_filename="f.mp3", model_name="tiny", device_info="CPU", - language_mode="detected", + language_mode=LANGUAGE_DETECTED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) assert "[00:00.00 - 00:02.00] Hello" in content @@ -88,7 +113,7 @@ def test_format_transcript_empty(): source_filename="silence.wav", model_name="tiny", device_info="CPU", - language_mode="detected", + language_mode=LANGUAGE_DETECTED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -113,12 +138,12 @@ def test_format_transcript_long(): source_filename="long.mp4", model_name="large-v3", device_info="CUDA", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 3, 17, 10, 0, 0), ) assert "**Длительность**: 01:03:20" in content - assert "**Язык**: en (forced)" in content + assert "**Язык**: en (задан явно)" in content # Timestamps should use hours format assert "[00:00:00.00 - 00:00:10.50] Начало." in content assert "[01:01:40.00 - 01:01:50.25] Конец." in content @@ -188,7 +213,7 @@ def test_format_transcript_tail_gap_warning(): source_filename="tail.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -210,7 +235,7 @@ def test_format_transcript_no_tail_gap_warning_for_small_gap(): source_filename="ok.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -231,7 +256,7 @@ def test_format_transcript_no_tail_gap_warning_for_exact_threshold(): source_filename="ok.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -257,7 +282,7 @@ def test_format_transcript_repetition_warning(): source_filename="repeat.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -284,7 +309,7 @@ def test_format_transcript_repetition_warning_uses_hours(): source_filename="long-repeat.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) @@ -305,7 +330,7 @@ def test_format_transcript_without_anomalies_has_no_warning_lines(): source_filename="ok.mp3", model_name="medium", device_info="CPU", - language_mode="forced", + language_mode=LANGUAGE_FORCED, transcription_date=datetime(2026, 1, 1, 0, 0, 0), ) diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index 2d1e611..b8783f6 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -5,7 +5,7 @@ import warnings import pytest from local_transcriber.backends.onnx_asr import OnnxAsrBackend -from local_transcriber.types import Segment, TranscribeResult +from local_transcriber.types import UNKNOWN_LANGUAGE, Segment, TranscribeResult class FakeVadSegment: @@ -390,7 +390,7 @@ class TestTranscribe: result = backend.transcribe(FakeModel(), wav_file, language=None) assert len(result.segments) == 0 - assert result.language == "unknown" + assert result.language == UNKNOWN_LANGUAGE assert result.duration == 1.0 def test_transcribe_skips_zero_length_vad_segments(self, monkeypatch, tmp_path): -- 2.54.0 From dae9d107c970e2cb8ca8c2c80dd78dd8181fe4ec Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 11:53:09 +0300 Subject: [PATCH 8/9] =?UTF-8?q?docs:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE-=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D0=B8=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=84=D0=B8=D0=BB=D1=8C=20turbo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - документация обещала large-v3-turbo на NVIDIA, где он недоступен, и умалчивала, что модель по умолчанию без CUDA понимает только русскую речь. - Что: - large-v3-turbo перенесён из таблицы faster-whisper в раздел OpenVINO с измеренными размерами моделей. - языковое ограничение авто-профиля и предупреждение CLI описаны в README, gpu.md и ADR-006. - в backlog добавлен пункт про turbo для faster-whisper, в gpu.md снято расхождение по скорости openvino-cpu. - Проверка: - вычитка diff, проверка якорной ссылки на docs/gpu.md. --- README.md | 40 ++++++++++++++++++++++++++++---- docs/PRD.md | 2 +- docs/adr/006-onnx-asr-backend.md | 12 +++++++--- docs/backlog.md | 16 +++++++++++++ docs/gpu.md | 13 ++++++++++- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 54ce456..76f70c7 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,11 @@ uv tool install git+https://github.com/dementev-dev/local-transcriber - **NVIDIA CUDA** (GPU): если есть GPU — транскрипция в 5-10× быстрее - **Windows**: `winget install -e --id Nvidia.CUDA --version 12.9` (от администратора), перезапустить терминал - **Linux / WSL2**: работает из коробки (нужен только драйвер: `nvidia-smi`) -- **Без NVIDIA GPU**: автоматически используется ONNX с GigaAM RNN-T на CPU +- **Без NVIDIA GPU**: автоматически используется ONNX с GigaAM RNN-T на CPU. + Эта модель понимает только русскую речь +- **Другие языки без NVIDIA**: выбирайте Whisper явно — + `--device openvino-cpu --model medium` на x86 или `--device cpu --model medium` + на любой платформе - **OpenVINO** для Intel GPU или x86 CPU остаётся доступен через явный `--device openvino`, `--device openvino-gpu` или `--device openvino-cpu` @@ -183,7 +187,7 @@ transcribe --uninstall-menu | Опция | Сокращение | По умолчанию | Описание | |-------|-----------|-------------|----------| | `--model` | `-m` | medium (CUDA) / gigaam-v3-e2e-rnnt (ONNX) | Модель распознавания | -| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.) | +| `--language` | `-l` | `ru` | Язык (ru, en, auto и др.); автоматический профиль без NVIDIA понимает только русский | | `--output` | `-o` | `<файл>-transcript.md` | Путь к выходному файлу | | `--device` | `-d` | `auto` | Устройство (auto, cpu, cuda, openvino, openvino-gpu, openvino-cpu, onnx) | | `--compute-type` | — | float16 (CUDA) / int8 (ONNX/OpenVINO) / float32 (CPU) | Тип вычислений | @@ -200,6 +204,9 @@ transcribe --uninstall-menu | OpenVINO (Intel GPU) | ✅ явно | — | ✅ явно | | GPU (NVIDIA) | ✅ авто | — | ✅ (нужен CUDA 12) | +Данные по macOS основаны на доступности пакетов onnxruntime: прогонов на этой +платформе не было. +
Linux / WSL2 @@ -253,7 +260,9 @@ language = "ru" При `device = "auto"` выбирается CUDA, если доступен `nvidia-smi`, иначе ONNX. Явный `device` из CLI или конфига отключает этот автоматический выбор. Каталоги моделей различаются между бэкендами, поэтому при закреплении `model` -в конфиге рекомендуется явно закрепить и совместимый `device`. +в конфиге рекомендуется явно закрепить и совместимый `device`. То же с языком: +автоматический ONNX-профиль рассчитан на русскую речь, а для остальных языков +нужен Whisper — например, `device = "openvino-cpu"` и `model = "medium"`. Дефолты зависят от устройства: @@ -278,6 +287,8 @@ language = "ru"
Таблица моделей +#### Whisper через faster-whisper (`--device cuda`, `--device cpu`) + | Модель | Размер на диске | VRAM (int8) | Скорость (GPU) | Качество | |--------|----------------|-------------|----------------|----------| | `tiny` | ~75 MB | ~1 GB | ★★★★★ | ★ | @@ -285,7 +296,15 @@ language = "ru" | `small` | ~460 MB | ~1.5 GB | ★★★ | ★★★ | | `medium` | ~1.5 GB | ~2.5 GB | ★★ | ★★★★ | | `large-v3` | ~3 GB | ~2.5 GB | ★ | ★★★★★ | -| `large-v3-turbo` | ~1 GB | — | ★★★★ | ★★★★ | + +#### Whisper через OpenVINO (`--device openvino-cpu`, `--device openvino-gpu`) + +Здесь те же модели Whisper, но предквантизированные, поэтому на диске они +занимают меньше места: `medium` int8 — 748 MB, `large-v3-turbo` int8 — 790 MB, +`large-v3-turbo` fp16 — 1552 MB. Модель `large-v3-turbo` доступна только здесь: +faster-whisper её не поддерживает, и запуск с `--device cuda` завершится +ошибкой. Полный список репозиториев — +[docs/gpu.md](docs/gpu.md#доступные-openvino-модели). #### ONNX-модели (`--device onnx`) @@ -318,6 +337,9 @@ language = "ru" кыргызский и узбекский внутри одной записи. `onnx-asr` не передаёт этим моделям подсказку языка, поэтому `--language` не управляет выбором языка. +Если модель не понимает запрошенный язык, CLI предупреждает об этом до начала +распознавания и подсказывает совместимый профиль, но работу не прерывает. + Для моделей из таблицы опубликованы `int8` и `float32`. Если неявный device-aware дефолт недоступен для выбранной модели, CLI сообщит о подстановке доступного варианта. Явное значение из `--compute-type` или @@ -359,7 +381,7 @@ device-aware дефолт недоступен для выбранной мод - **Дата транскрипции**: 2026-03-17 14:30:05 - **Модель**: large-v3 -- **Язык**: ru (detected) +- **Язык**: ru (задан явно) - **Длительность**: 01:23:45 - **Устройство**: CUDA (NVIDIA GeForce RTX 3060) @@ -374,6 +396,14 @@ device-aware дефолт недоступен для выбранной мод Близкие по времени сегменты автоматически объединяются в абзацы (пауза > 2 сек или длительность > 60 сек разделяет абзацы). Таймкоды: `MM:SS.ss`, для записей длиннее 1 часа — `HH:MM:SS.ss`. +В скобках после языка указан его источник: + +- `задан явно` — язык взят из `--language` или конфига; +- `определён автоматически` — распознан моделью при `--language auto`; +- `из профиля модели` — у модели всего один язык, как у GigaAM. + +Если язык определить не удалось, строка выглядит так: `- **Язык**: не определён`. +
## Поддерживаемые форматы diff --git a/docs/PRD.md b/docs/PRD.md index 30e137a..b862a87 100644 --- a/docs/PRD.md +++ b/docs/PRD.md @@ -64,7 +64,7 @@ transcribe <путь_к_файлу> [опции] - **Дата транскрипции**: 2026-03-17 14:30:05 - **Модель**: large-v3 -- **Язык**: ru (detected) / ru (forced) +- **Язык**: ru (задан явно) / ru (определён автоматически) - **Длительность**: 01:23:45 - **Устройство**: CUDA (NVIDIA GeForce RTX 3060) diff --git a/docs/adr/006-onnx-asr-backend.md b/docs/adr/006-onnx-asr-backend.md index c52d1cd..33a3cbf 100644 --- a/docs/adr/006-onnx-asr-backend.md +++ b/docs/adr/006-onnx-asr-backend.md @@ -107,6 +107,11 @@ GigaAM v3 E2E RNN-T — модель по умолчанию на машинах Порядок `--device auto`: CUDA при наличии `nvidia-smi`, иначе ONNX. OpenVINO и FasterWhisper CPU остаются доступными через явный CLI-аргумент или конфиг. +GigaAM понимает только русскую речь, поэтому для остальных языков автоматический +профиль не годится — нужен явный Whisper. Несовместимый язык работу не +останавливает: CLI предупреждает о нём до начала распознавания и называет +подходящий профиль. + Модели: - **`gigaam-v3-e2e-rnnt`** — модель по умолчанию: практически равна обычному GigaAM по скорости, немного уступает по WER, но выдаёт готовую пунктуацию для @@ -132,10 +137,11 @@ FasterWhisper CPU остаются доступными через явный CL ## Открытые вопросы / следующие шаги -- **Galлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю. +- **Галлюцинации Whisper medium на длинных файлах** — отдельный продуктовый риск, требующий собственного исследования. Возможно, имеет смысл ограничить максимальную длину чанка для openvino-medium, или дать предупреждение пользователю. - **Canary** (`nemo-canary-1b-v2`) — тяжелее, но multilingual + пунктуация. Кандидат на «лучшее качество за разумную скорость» для тех, кому важна пунктуация. -- Проверить профиль на других языках и при необходимости добавить явные - многоязычные рекомендации; автоматическая политика намеренно не зависит от языка. +- Проверить качество на других языках и при необходимости дополнить + многоязычные рекомендации. Сама автоматическая политика от языка не зависит: + она предупреждает о несовместимости, но профиль за пользователя не меняет. ## Отклонённые альтернативы diff --git a/docs/backlog.md b/docs/backlog.md index 11090e9..f7d82a1 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -252,6 +252,22 @@ SQL`), словарь пользовательский в `.transcriber.toml`. --- +### `large-v3-turbo` для faster-whisper + +**Что:** Добавить `large-v3-turbo` в каталог faster-whisper, чтобы модель была +доступна с `--device cuda` и `--device cpu`, а не только через OpenVINO. + +**Почему:** На CPU turbo INT8 оказался быстрее `medium` и точнее по WER +([сравнение от 2026-08-12](benchmarks/2026-08-12-openvino-large-v3-turbo-comparison.md)), +и владельцам NVIDIA этот профиль сейчас недоступен без ручного указания +репозитория HuggingFace. + +**Почему откладывается:** каталог faster-whisper в проекте собран из +репозиториев `Systran`, и для turbo нужно сначала выяснить, какая CT2-сборка +годится, проверить её происхождение и качество, и только потом вносить в код. + +--- + ## Отклонённые направления *(пока пусто — добавлять сюда то, что попробовали и решили не делать, с причиной)* diff --git a/docs/gpu.md b/docs/gpu.md index 1fb94c0..8b7f898 100644 --- a/docs/gpu.md +++ b/docs/gpu.md @@ -6,7 +6,7 @@ - `cuda` — строго NVIDIA GPU, ошибка если недоступен - `openvino` — авто-выбор OpenVINO GPU или CPU - `openvino-gpu` — строго Intel GPU через OpenVINO -- `openvino-cpu` — строго CPU через OpenVINO (ускорение 2-4x на x86) +- `openvino-cpu` — строго CPU через OpenVINO (3-10x на x86, зависит от модели) - `cpu` — строго CPU (faster-whisper/CTranslate2) ## Какой бэкенд на каком оборудовании @@ -22,6 +22,10 @@ \* По результатам контрольных прогонов на Intel и AMD CPU. Реальная скорость зависит от CPU, модели и записи. +Автоматический профиль без NVIDIA рассчитан на русскую речь: GigaAM других +языков не понимает. Для них берите Whisper — `openvino-cpu` на x86 или `cpu` +на любой платформе. + ## OpenVINO OpenVINO ускоряет inference на x86 процессорах (Intel и AMD) через оптимизированные инструкции @@ -44,6 +48,13 @@ OpenVINO ускоряет inference на x86 процессорах (Intel и AM | large-v3 | OpenVINO/whisper-large-v3-int8-ov | OpenVINO/whisper-large-v3-fp16-ov | | large-v3-turbo | OpenVINO/whisper-large-v3-turbo-int8-ov | OpenVINO/whisper-large-v3-turbo-fp16-ov | +Размер в кеше HuggingFace: `medium` int8 — 748 MB, `large-v3-turbo` int8 — +790 MB, `large-v3-turbo` fp16 — 1552 MB. Это меньше, чем у тех же моделей для +faster-whisper, потому что веса уже квантизированы. + +Модель `large-v3-turbo` доступна только в OpenVINO: для `--device cuda` и +`--device cpu` каталог faster-whisper заканчивается на `large-v3`. + ### Результаты тестирования OpenVINO Актуальное сравнение OpenVINO 2026.3 на трёх русскоязычных встречах: -- 2.54.0 From 9ae013ad0f7c1829c203042b6c3c2ad64b556ba3 Mon Sep 17 00:00:00 2001 From: Dmitriy Dementiev Date: Wed, 12 Aug 2026 12:19:07 +0300 Subject: [PATCH 9/9] =?UTF-8?q?fix(onnx):=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=BA=D0=B8=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BB=D0=B0?= =?UTF-8?q?=D0=B3=D0=B0=D1=8E=D1=82=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF?= =?UTF-8?q?=D0=BD=D1=8B=D0=B9=20=D0=BD=D0=B0=20=D0=BF=D0=BB=D0=B0=D1=82?= =?UTF-8?q?=D1=84=D0=BE=D1=80=D0=BC=D0=B5=20=D0=B1=D1=8D=D0=BA=D0=B5=D0=BD?= =?UTF-8?q?=D0=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - подсказки в ошибке и предупреждении называли только OpenVINO и CUDA, которых нет на macOS и ARM, поэтому пользователю там предлагались заведомо нерабочие команды. - Что: - в оба сообщения добавлен вариант --device cpu, работающий на любой платформе. - для large-v3-turbo подсказки по cpu и cuda подставляют medium, так как faster-whisper turbo не поддерживает. - Проверка: - uv run pytest -q: 243 passed, 1 skipped. --- src/local_transcriber/backends/onnx_asr.py | 23 +++++++++++++++++----- tests/test_onnx_asr.py | 10 ++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/local_transcriber/backends/onnx_asr.py b/src/local_transcriber/backends/onnx_asr.py index 5946ea1..c73212b 100644 --- a/src/local_transcriber/backends/onnx_asr.py +++ b/src/local_transcriber/backends/onnx_asr.py @@ -55,6 +55,9 @@ _PARAKEET_V3_LANGUAGES = frozenset( _WHISPER_MODEL_NAMES = frozenset( {"tiny", "base", "small", "medium", "large-v3", "large-v3-turbo"} ) +# faster-whisper не знает turbo, поэтому для cpu и cuda подсказываем medium. +# Источник правды — MODEL_REPOS в backends/faster_whisper.py и backends/openvino.py +_OPENVINO_ONLY_WHISPER_MODELS = frozenset({"large-v3-turbo"}) MODEL_CATALOG: dict[str, OnnxModelSpec] = { "gigaam-v3": OnnxModelSpec( @@ -253,11 +256,13 @@ class OnnxAsrBackend: if model_name in MODEL_ALIASES: return MODEL_ALIASES[model_name] if model_name in _WHISPER_MODEL_NAMES: + fallback = _whisper_fallback_model(model_name) raise ValueError( f"Модель '{model_name}' относится к Whisper и не поддерживается " "ONNX-бэкендом. Без CUDA --device auto выбирает ONNX; " - f"укажите --device openvino-cpu --model {model_name} " - "или --device cuda --model medium." + f"укажите --device openvino-cpu --model {model_name} на x86, " + f"--device cpu --model {fallback} на любой платформе " + f"или --device cuda --model {fallback} при NVIDIA GPU." ) if "/" in model_name or model_name.count("-") >= 2: # Looks like a raw onnx-asr name — allow passthrough @@ -280,9 +285,10 @@ class OnnxAsrBackend: warnings.warn( f"Язык '{language}' не поддерживается моделью '{self._model_name}' " f"(поддерживаются: {supported}). Результат может быть некорректным. " - "Для других языков используйте " - "--device openvino-cpu --model medium " - "или --device cuda --model medium.", + "Для других языков возьмите Whisper: " + "--device openvino-cpu --model medium на x86, " + "--device cpu --model medium на любой платформе " + "или --device cuda --model medium при NVIDIA GPU.", UserWarning, stacklevel=2, ) @@ -304,6 +310,13 @@ def _preferred_compute_type(quantizations: frozenset[str | None]) -> str: raise ValueError("Для ONNX-модели не указаны доступные квантизации") +def _whisper_fallback_model(model_name: str) -> str: + """Модель для подсказки про faster-whisper: turbo там недоступен.""" + if model_name in _OPENVINO_ONLY_WHISPER_MODELS: + return "medium" + return model_name + + 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)) diff --git a/tests/test_onnx_asr.py b/tests/test_onnx_asr.py index b8783f6..d29aeb0 100644 --- a/tests/test_onnx_asr.py +++ b/tests/test_onnx_asr.py @@ -257,6 +257,7 @@ class TestTranscribe: UserWarning, match=( r"Язык 'en'.*--device openvino-cpu --model medium.*" + r"--device cpu --model medium.*" r"--device cuda --model medium" ), ): @@ -460,6 +461,13 @@ class TestModelAliases: ): backend._resolve_model("medium") + def test_whisper_error_offers_platform_independent_backend(self): + """На macOS и ARM нет ни OpenVINO, ни CUDA — нужен путь через cpu.""" + backend = OnnxAsrBackend() + + with pytest.raises(ValueError, match=r"--device cpu --model medium"): + backend._resolve_model("medium") + def test_turbo_whisper_error_suggests_models_supported_by_backends(self): backend = OnnxAsrBackend() @@ -469,4 +477,6 @@ class TestModelAliases: message = str(exc_info.value) assert "--device openvino-cpu --model large-v3-turbo" in message assert "--device cuda --model medium" in message + assert "--device cpu --model medium" in message assert "--device cuda --model large-v3-turbo" not in message + assert "--device cpu --model large-v3-turbo" not in message -- 2.54.0