"""Конфигурация приложения, загруженная из переменных окружения / файла .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 " 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()