Files
vidconf/backend/core/config.py

139 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Конфигурация приложения, загруженная из переменных окружения / файла .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` по выбранному пресету (15), 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, пресеты 14;
`.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()