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