"""Проверка и докачка моделей при запуске. Обновление возит только папку app, поэтому смена режима работы может потребовать файла, которого на машине нет: так вышло с детектором речи, и сервис узнал об этом на первой же задаче. Список моделей и их адреса живут здесь, внутри app, и приезжают вместе с кодом. """ import logging from collections.abc import Callable from pathlib import Path __all__ = ["required_models", "missing_models", "ensure_models"] log = logging.getLogger("talkscore-asr") RELEASES = "https://github.com/k2-fsa/sherpa-onnx/releases/download" # Модель распознавания качается отдельно: она весит сотни мегабайт и лежит # на Hugging Face, докачивать её молча при запуске неправильно. DOWNLOADABLE = { "silero_vad.onnx": f"{RELEASES}/asr-models/silero_vad.onnx", "nemo_en_titanet_small.onnx": f"{RELEASES}/speaker-recongition-models/nemo_en_titanet_small.onnx", } def required_models(models_dir: Path, diarize: bool) -> list[Path]: """Что должно лежать на диске при текущих настройках.""" needed = [models_dir / "gigaam" / "config.json"] if diarize: needed.append(models_dir / "sherpa-onnx-pyannote-segmentation-3-0" / "model.onnx") needed.append(models_dir / "nemo_en_titanet_small.onnx") else: needed.append(models_dir / "silero_vad.onnx") return needed def missing_models(models_dir: Path, diarize: bool) -> list[Path]: return [path for path in required_models(models_dir, diarize) if not path.is_file()] def ensure_models(models_dir: Path, diarize: bool, fetch: Callable[[str, Path], None] | None = None) -> list[Path]: """Докачивает мелкие модели, которых не хватает. Возвращает оставшиеся. Скачиваются только те, что весят единицы мегабайт: ждать их при запуске не накладно. Всё крупное остаётся на download_models.bat - тянуть сотни мегабайт без спроса нельзя. """ # Загрузчик передаётся параметром, а не берётся из глобальной области: # подмену глобальной функции в тестах перебивает порядок импортов. fetch = fetch or _fetch still_missing: list[Path] = [] for path in missing_models(models_dir, diarize): url = DOWNLOADABLE.get(path.name) if url is None: still_missing.append(path) continue try: fetch(url, path) print(f" Докачана модель: {path.name}") except Exception as exc: # noqa: BLE001 - причина неважна, важен внятный отказ log.warning("не удалось скачать %s: %s", path.name, exc) still_missing.append(path) return still_missing def _fetch(url: str, dst: Path) -> None: """Скачивает файл во временное имя и переименовывает. Встраиваемый Python идёт без хранилища сертификатов, поэтому берём requests с certifi, а не urllib. """ import requests dst.parent.mkdir(parents=True, exist_ok=True) tmp = dst.with_suffix(dst.suffix + ".part") with requests.get(url, stream=True, timeout=120) as response: response.raise_for_status() with open(tmp, "wb") as handle: for chunk in response.iter_content(chunk_size=1 << 16): handle.write(chunk) # Переименование в конце: оборванная закачка не должна выглядеть моделью. tmp.replace(dst)