Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

138
backend/core/config.py Normal file
View File

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