139 lines
7.7 KiB
Python
139 lines
7.7 KiB
Python
"""Конфигурация приложения, загруженная из переменных окружения / файла .env."""
|
||
|
||
from functools import lru_cache
|
||
|
||
from pydantic import field_validator
|
||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||
|
||
from core.plugins.config import AiLevel
|
||
|
||
|
||
class Settings(BaseSettings):
|
||
"""Центральные параметры приложения.
|
||
|
||
Значения читаются из переменных окружения (или файла `.env`).
|
||
"""
|
||
|
||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||
|
||
database_url: str = "postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf"
|
||
redis_url: str = "redis://localhost:6379/0"
|
||
plugins_config_path: str = "../config/plugins.yaml"
|
||
|
||
# --- Версия инстанса (релиз v0.0.1) ---
|
||
# install.sh копирует значение из файла `VERSION` (корень репозитория) в
|
||
# `.env` при каждой установке/обновлении — здесь только чтение готового
|
||
# значения. Отдаётся в `GET /api/health` (футер админки, Блок 4).
|
||
vidconf_version: str = "0.0.0"
|
||
|
||
# Домен не должен попадать в список special-use/reserved (RFC 6761,
|
||
# напр. `.local`/`.test`): email-validator (`EmailStr`) их отклоняет, а
|
||
# раньше это ловилось и на выходе — старый дефолт `admin@vidconf.local`
|
||
# ронял `GET /users/me` 500 `ResponseValidationError`, пока `UserOut.email`
|
||
# был `EmailStr`. `.example` (RFC 2606) email-validator пропускает.
|
||
seed_admin_email: str = "admin@vidconf.example"
|
||
seed_admin_password: str = "change-me"
|
||
|
||
# --- Auth (JWT + email-подтверждение) ---
|
||
jwt_secret: str = "dev-only-insecure-secret-change-me"
|
||
access_token_ttl_minutes: int = 15
|
||
refresh_token_ttl_days: int = 14
|
||
email_verification_ttl_hours: int = 24
|
||
frontend_url: str = "http://localhost:5173"
|
||
# Флаг Secure для refresh-cookie. false нужен только для dev по
|
||
# http://localhost (Safari, в отличие от Chrome, не сохраняет
|
||
# Secure-cookie без HTTPS); в проде обязательно true.
|
||
auth_cookie_secure: bool = True
|
||
|
||
# --- LiveKit ---
|
||
livekit_api_key: str = "devkey"
|
||
livekit_api_secret: str = "change-me-livekit-secret"
|
||
livekit_public_url: str = "ws://localhost:7880"
|
||
# Внутренний server-to-server URL для вызовов LiveKit RoomService (Celery
|
||
# maintenance-задача); в отличие от `livekit_public_url` не проксируется
|
||
# через nginx/TLS для браузера. LiveKit SDK сам нормализует ws:// в http://.
|
||
livekit_url: str = "ws://localhost:7880"
|
||
|
||
# --- Пайплайн транскрибации ---
|
||
# Общий volume между LiveKit Egress и celery-воркером `transcription`
|
||
# (см. `deploy/docker-compose.yml`); в тестах переопределяется на `tmp_path`.
|
||
recordings_dir: str = "/recordings"
|
||
|
||
# --- Email (SMTP-бэкенд) ---
|
||
# `console` — дефолт для dev (письмо только логируется); `smtp` — реальная
|
||
# отправка через aiosmtplib. Секреты SMTP — только в `.env` (инвариант №6),
|
||
# переключатель бэкенда — тоже переменная окружения, а не настройка в БД
|
||
# (`instance_settings`).
|
||
email_backend: str = "console"
|
||
smtp_host: str = "localhost"
|
||
smtp_port: int = 587
|
||
smtp_username: str | None = None
|
||
smtp_password: str | None = None
|
||
smtp_start_tls: bool = True
|
||
smtp_use_tls: bool = False
|
||
smtp_from: str = "VidConf <no-reply@vidconf.example>"
|
||
smtp_timeout_s: int = 30
|
||
|
||
# --- Медиа (аватары пользователей) ---
|
||
# Каталог, куда сохраняются загруженные файлы (аватары — `avatars/{user_id}.{ext}`);
|
||
# раздаётся статикой по `/media` (`main.py`, dev) либо через nginx `location /media/`
|
||
# в проде (`deploy/nginx/nginx.conf`, volume `media`). Относительный путь по
|
||
# умолчанию — рабочая директория backend (аналог `recordings_dir`, но без
|
||
# требования root для локального запуска вне Docker).
|
||
media_root: str = "media"
|
||
|
||
# --- Автодетект железа: install.sh определяет `nproc`/`free -m`/
|
||
# `nvidia-smi` и пишет в `.env`; читает `services/ai_levels.py` для детекта
|
||
# доступности уровней AI (ADR-004) без torch/nvidia-smi внутри процесса
|
||
# backend/воркеров. `None` — install.sh не запускался (dev-окружение) либо
|
||
# GPU не обнаружен (`hw_gpu_name`/`hw_vram_mb`).
|
||
hw_cpus: int | None = None
|
||
hw_ram_mb: int | None = None
|
||
hw_gpu_name: str | None = None
|
||
hw_vram_mb: int | None = None
|
||
|
||
# --- Матрица «пресет → настройки» инсталлятора: install.sh пишет эти три
|
||
# переменные в `.env` по выбранному пресету (1–5), lifespan backend
|
||
# передаёт их бутстрапу `instance_settings` (`services/instance_settings.py`,
|
||
# `bootstrap_overrides_from_settings`) как overrides дефолтов
|
||
# `plugins.yaml` — БЕЗ этого механизма бутстрап всегда включал чат и
|
||
# AI-модули независимо от пресета. `None` — install.sh не запускался
|
||
# (dev-окружение) либо переменная не установлена для этого пресета:
|
||
# бутстрап тогда использует дефолты `plugins.yaml` как раньше.
|
||
bootstrap_chat_enabled: bool | None = None
|
||
bootstrap_transcription_enabled: bool | None = None
|
||
bootstrap_ai_level: AiLevel | None = None
|
||
|
||
@field_validator(
|
||
"hw_cpus",
|
||
"hw_ram_mb",
|
||
"hw_gpu_name",
|
||
"hw_vram_mb",
|
||
"bootstrap_chat_enabled",
|
||
"bootstrap_transcription_enabled",
|
||
"bootstrap_ai_level",
|
||
mode="before",
|
||
)
|
||
@classmethod
|
||
def _empty_hw_string_to_none(cls, value: object) -> object:
|
||
"""Пустая строка env (`KEY=`, а не отсутствие переменной) → `None`.
|
||
|
||
`docker-compose` подставляет `env_file` дословно: `HW_VRAM_MB=` в `.env`
|
||
(пишет `install.sh` на любой машине без NVIDIA GPU, пресеты 1–4;
|
||
`.env.example` — все четыре `HW_*` пустыми по умолчанию) превращается в
|
||
переменную окружения со значением `""`, а не в отсутствующую переменную —
|
||
без этой нормализации pydantic не парсит `""` как `int` и роняет
|
||
`Settings()` уже на импорте модуля (`main.py`, `workers/celery_app.py`),
|
||
не давая контейнеру стартовать. Та же проблема для `BOOTSTRAP_*`
|
||
(`.env.example` — пустыми по умолчанию, install.sh заполняет по пресету).
|
||
"""
|
||
if value == "":
|
||
return None
|
||
return value
|
||
|
||
|
||
@lru_cache
|
||
def get_settings() -> Settings:
|
||
"""Вернуть кэшированный экземпляр `Settings`."""
|
||
return Settings()
|