Обновление возит только папку app, поэтому переход на детектор речи потребовал файла, которого на машине нет - и сервис узнал об этом на первой же задаче: отказ выглядел сбоем обработки, хотя это незаконченная установка. Причём download_models.py лежит вне app и тоже был старым, так что штатный способ докачки не помог бы. Список моделей и адреса переехали в app/models.py. Мелкие докачиваются при запуске, крупная модель распознавания - нет: сотни мегабайт без спроса тянуть нельзя. Нехватка видна в /health полем missing_models. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
84 lines
4.1 KiB
Python
84 lines
4.1 KiB
Python
"""Проверка и докачка моделей при запуске.
|
||
|
||
Обновление возит только папку 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)
|