first commit
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled

This commit is contained in:
2026-07-23 02:38:05 +03:00
commit 8757bec8ac
335 changed files with 61527 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
"""Определение доступности уровней AI-модуля (`min`/`medium`/`max`) инстанса.
Матрица уровней (модели, требования RAM/GPU/VRAM, пути моделей на дисковых
томах) — константа `TIERS` (`services/ai_tiers.py`), единственный источник
истины — ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`). Детект
читает обнаруженное `install.sh` железо (`HW_*` в `.env`, `core.config.Settings`)
и факт наличия файлов моделей на дисковых томах — без зависимости от
torch/nvidia-smi внутри процесса backend/воркеров (переменные пишет установщик,
а не рантайм-детект GPU).
"""
from pathlib import Path
from pydantic import BaseModel
from core.config import Settings, get_settings
from core.plugins.config import AiLevel, InstanceConfig
from services.ai_tiers import TIERS, WHISPER_MODELS_ROOT, TierSpec
_PRESET_BY_LEVEL: dict[AiLevel, int] = {"min": 3, "medium": 4, "max": 5}
"""Номер пресета инсталлятора, соответствующего уровню (ADR-004, таблица
требований железа) — используется в тексте причины недоступности."""
class AiLevelStatus(BaseModel):
"""Доступность одного уровня AI с человекочитаемой причиной отказа."""
level: AiLevel
available: bool
reason: str | None = None
def detect_ai_levels(cfg: InstanceConfig) -> list[AiLevelStatus]:
"""Вернуть статусы всех уровней AI по обнаруженному железу и скачанным моделям.
`cfg` пока не влияет на результат (доступность уровня зависит только от
железа и файлов моделей на диске, не от текущих настроек инстанса), но
остаётся параметром сигнатуры — используется и `api/admin.py`, и
`services/instance_settings.py::update`, где эффективная конфигурация уже
под рукой.
"""
settings = get_settings()
statuses: list[AiLevelStatus] = []
for level in ("min", "medium", "max"):
reasons = _unavailability_reasons(level, TIERS[level], settings)
statuses.append(
AiLevelStatus(level=level, available=not reasons, reason="; ".join(reasons) or None)
)
return statuses
def _unavailability_reasons(level: AiLevel, spec: TierSpec, settings: Settings) -> list[str]:
"""Собрать причины недоступности уровня `level` (пустой список — уровень доступен)."""
reasons: list[str] = []
if settings.hw_ram_mb is not None and settings.hw_ram_mb < spec.min_ram_mb:
reasons.append(f"недостаточно RAM: нужно {spec.min_ram_mb // 1024} ГБ")
if spec.requires_gpu:
required_vram_gb = (spec.min_vram_mb or 0) // 1024
if not settings.hw_gpu_name:
reasons.append(f"требуется GPU NVIDIA ≥{required_vram_gb} ГБ VRAM, не обнаружен")
elif spec.min_vram_mb is not None and (settings.hw_vram_mb or 0) < spec.min_vram_mb:
found_vram_gb = (settings.hw_vram_mb or 0) // 1024
reasons.append(
f"требуется GPU NVIDIA ≥{required_vram_gb} ГБ VRAM, "
f"обнаружено только {found_vram_gb} ГБ"
)
preset = _PRESET_BY_LEVEL[level]
for path in spec.model_files:
if not _model_downloaded(path):
reasons.append(
f"{_describe_model_file(path)} не скачана — запустите install.sh "
f"с пресетом {preset}"
)
return reasons
def _model_downloaded(path: str) -> bool:
"""Проверить, скачана ли модель по пути на томе.
Файл — непустой; каталог (например, кэш huggingface_hub с хэшированными
поддиректориями снапшотов) — непустой каталог, без проверки конкретных
вложенных файлов.
"""
p = Path(path)
if p.is_dir():
return any(p.iterdir())
return p.is_file() and p.stat().st_size > 0
def _describe_model_file(path: str) -> str:
"""Человекочитаемое имя модели для причины недоступности («модель транскрибации small»)."""
name = Path(path).name
kind = "транскрибации" if path.startswith(f"{WHISPER_MODELS_ROOT}/") else "суммаризации"
return f"модель {kind} {name}"