From c1ba61fcd58fc14bba6b0b590a3e96aba6cbfe83 Mon Sep 17 00:00:00 2001 From: Vladimir Bryzgalov Date: Sat, 15 Aug 2026 22:51:54 +0500 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=BE=D1=80=D0=BA=D0=B5=D1=80=D1=8B=20?= =?UTF-8?q?=D0=B3=D1=80=D1=83=D0=B7=D1=8F=D1=82=20=D0=BC=D0=BE=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8=20=D0=BF=D1=80=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B2?= =?UTF-8?q?=D0=BE=D0=B9=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Прогрет был только нулевой воркер, остальные получали пустой конвейер и роняли задачу с "модели не загружены". В комментарии было написано, что они греются сами, но кода для этого не было. Заодно отключены ANSI-цвета uvicorn на Windows: консоль их не разбирает и печатала управляющие последовательности как мусор. Co-Authored-By: Claude Opus 5 (1M context) --- app/main.py | 5 ++++- app/pipeline.py | 8 ++++++-- app/version.py | 2 +- tests/test_concurrency.py | 40 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/app/main.py b/app/main.py index 6bf9234..e2e8113 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,6 @@ """HTTP-сервис распознавания: приём файла, очередь, выдача результата.""" import logging +import os import shutil import sys import tempfile @@ -294,7 +295,9 @@ def run() -> None: f"\n Потоков: {settings.effective_threads()}" f"\n Проверка: curl http://localhost:{settings.port}/health" "\n Остановить: Ctrl+C\n") - uvicorn.run(app, host=settings.host, port=settings.port, log_level="info") + # Windows-консоль не разбирает ANSI-последовательности и печатает их как мусор + uvicorn.run(app, host=settings.host, port=settings.port, log_level="info", + use_colors=(os.name != "nt")) if __name__ == "__main__": diff --git a/app/pipeline.py b/app/pipeline.py index b8db12b..60b21c4 100644 --- a/app/pipeline.py +++ b/app/pipeline.py @@ -149,9 +149,13 @@ class Pipeline: return diar def transcribe(self, wav_path: Path, num_speakers: int = 2) -> dict: - """Полный проход: диаризация, распознавание реплик, постобработка.""" + """Полный проход: диаризация, распознавание реплик, постобработка. + + Модели грузятся при первой задаче, а не при создании: воркеров несколько, + и держать копии моделей у тех, кому работа ещё не досталась, незачем. + """ if self._asr is None: - raise RuntimeError("модели не загружены, вызовите warmup()") + self.warmup() self._reload_replacements() samples = read_wav(wav_path) diff --git a/app/version.py b/app/version.py index d3ec452..493f741 100644 --- a/app/version.py +++ b/app/version.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.3.0" diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 45bc05c..97cab63 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -95,3 +95,43 @@ class TestWorkerSettings: config = tmp_path / "config.toml" config.write_text('[processing]\nworkers=3\n', encoding="utf-8") assert load_settings(config).effective_workers() == 3 + + +class TestLazyWarmup: + """Воркеры создаются заранее, а модели грузят при первой своей задаче.""" + + def test_transcribe_warms_up_when_needed(self, tmp_path, monkeypatch): + import sys + import types + + for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"): + monkeypatch.setitem(sys.modules, name, types.ModuleType(name)) + from app.pipeline import Pipeline + + p = Pipeline(models_dir=tmp_path, threads=1, + replacements_path=tmp_path / "r.txt", base_dir=tmp_path) + called = {"warmup": 0} + monkeypatch.setattr(p, "warmup", lambda: called.__setitem__("warmup", 1)) + # transcribe упадёт дальше на чтении файла, но warmup обязан быть вызван + with pytest.raises(Exception): + p.transcribe(tmp_path / "нет.wav") + assert called["warmup"] == 1 + + def test_no_runtime_error_about_warmup(self, tmp_path, monkeypatch): + """Прежде здесь падало «модели не загружены, вызовите warmup()».""" + import sys + import types + + for name in ("sherpa_onnx", "onnx_asr", "onnxruntime"): + monkeypatch.setitem(sys.modules, name, types.ModuleType(name)) + from app.pipeline import Pipeline + + p = Pipeline(models_dir=tmp_path, threads=1, + replacements_path=tmp_path / "r.txt", base_dir=tmp_path) + monkeypatch.setattr(p, "warmup", lambda: None) + try: + p.transcribe(tmp_path / "нет.wav") + except RuntimeError as exc: + assert "warmup" not in str(exc) + except Exception: + pass