lct-hack/backend/app/config.py

91 lines
5 KiB
Python
Raw Normal View History

"""Настройки. Нормативы ГОСТ — в миллисекундах и из конфига, не из кода."""
from functools import lru_cache
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.domain.timers import NORMATIVES, TimerCode
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
# Данные
database_url: str = "postgresql+asyncpg://lct:lct@localhost:5432/lct"
# Только локальная демонстрация: живые занятия и отчёты в памяти, без
# Postgres и без долговременного журнала. Не включать на учебном стенде.
demo_no_db: bool = False
# Голосовой контур
models_dir: str = "models"
stt_model: str = "whisper-small"
# Локальный файл GGML Whisper small для whisper.cpp. Имя модели из сети
# не принимается: учебный контур не скачивает веса при запуске.
whisper_model_path: str = "models/whisper-small"
whisper_server_url: str = "http://127.0.0.1:18082"
endpointing_ms: int = 600
mic_sample_rate: int = 16_000
tts_sample_rate: int = 24_000
offline: bool = False
# STT/голос не входят в MVP ДДС; включаются лишь для отдельного эксперимента.
voice_enabled: bool = False
# Запись хранится локально и отдаётся только через авторизованный API.
record_calls: bool = True
recordings_dir: str = "recordings"
# Автокопирование полного стенда. Интервал по умолчанию ровно сутки;
# небольшой grace нужен только для отображения просрочки в мониторинге.
backup_interval_seconds: int = 86_400
backup_retry_seconds: int = 300
backup_keep: int = 14
# LLM. Провайдер меняется значением, не кодом (docs/arch/STACK.md).
# Имена COMPAT_MODEL_* принимаются тоже — так их выставляет командный сниппет.
llm_provider: str = "local"
llm_base_url: str = Field(
default="http://127.0.0.1:18080/v1",
validation_alias=AliasChoices("llm_base_url", "compat_model_url"),
)
llm_api_key: str = Field(
default="", validation_alias=AliasChoices("llm_api_key", "compat_model_api_key"),
repr=False,
)
llm_model_caller: str = Field(
default="Qwen3-1.7B", validation_alias=AliasChoices("llm_model_caller", "compat_model_name")
)
llm_model_judge: str = Field(
default="", validation_alias=AliasChoices("llm_model_judge", "compat_model_name")
)
judge_temperature: float = 0.0
# Vikhr — отдельный русскоязычный контрольный профиль диалога. Никакая
# модель не решает, какие факты раскрыть, как маршрутизировать карточку или
# какой балл поставить: это делает протокол и оценка в коде.
llm_control_base_url: str = "http://127.0.0.1:18081/v1"
llm_model_control: str = "Vikhr-1B"
grammar_llm_enabled: bool = False
dialogue_model_mode: str = "dialogue" # dialogue | russian_control
# Docker Desktop даёт контейнеру специальное имя хоста. Оно разрешается
# только явным флагом: обычный OFFLINE по-прежнему принимает лишь literal
# loopback и не может незаметно уйти во внешний API.
allow_docker_host_models: bool = False
feat: вход, роли и аудит действий (lct-23) Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались ссылкой с номером занятия, и пускало знание адреса. - Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная cookie; роль на сокетах читается из той же cookie в момент рукопожатия, отдельного протокола авторизации в канале нет. - Разграничение: control — преподавателю, observe — преподавателю и админу, call и station — обучающемуся и преподавателю. Отказ приходит событием error с кодом forbidden. - Обучающийся не видит чужого: история подменяет фильтр на его собственный идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ к чужим результатам, а не только к чужим экранам. - Администратору закрыта правка оценок — ТЗ запрещает это прямо. - make users заводит по записи на роль и печатает случайные пароли один раз: зашитый в репозиторий admin/admin пережил бы сдачу. - Экран входа и проверка роли на каждом маршруте фронта. Наши инструменты не сломались: make lesson и тесты входят через dev-token за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной функции не должно быть видно вовсе. У тестов появился conftest.py. Role уехала в домен и в generated.ts через EventCatalog.principal: иначе фронт переписывал бы список ролей руками. 181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
# Вход и роли (lct-23). `session_secret` обязателен на рабочем стенде:
# значение по умолчанию годится только для разработки, и администратор
# видит в состоянии сервисов, что оно не заменено.
session_secret: str = "dev-secret-поменять-на-стенде"
# В TLS-профиле cookie уходит только по HTTPS/WSS. Для нативной разработки
# на loopback флаг выключен, иначе браузер не сможет войти по HTTP.
secure_cookies: bool = False
feat: вход, роли и аудит действий (lct-23) Самое крупное расхождение с ТЗ: входа не было вовсе, экраны открывались ссылкой с номером занятия, и пускало знание адреса. - Таблицы users и audit_log, миграция. Пароль argon2, сессия — подписанная cookie; роль на сокетах читается из той же cookie в момент рукопожатия, отдельного протокола авторизации в канале нет. - Разграничение: control — преподавателю, observe — преподавателю и админу, call и station — обучающемуся и преподавателю. Отказ приходит событием error с кодом forbidden. - Обучающийся не видит чужого: история подменяет фильтр на его собственный идентификатор, разбор и профиль сверяют trainee_id. ТЗ запрещает доступ к чужим результатам, а не только к чужим экранам. - Администратору закрыта правка оценок — ТЗ запрещает это прямо. - make users заводит по записи на роль и печатает случайные пароли один раз: зашитый в репозиторий admin/admin пережил бы сдачу. - Экран входа и проверка роли на каждом маршруте фронта. Наши инструменты не сломались: make lesson и тесты входят через dev-token за флагом dev_auth_bypass, на стенде точка отвечает 404 — выключенной функции не должно быть видно вовсе. У тестов появился conftest.py. Role уехала в домен и в generated.ts через EventCatalog.principal: иначе фронт переписывал бы список ролей руками. 181 тест зелёный (14 новых), make typecheck чистый.
2026-09-20 09:01:05 +03:00
#: Вход без пароля для `make lesson` и тестов. На стенде выключен.
dev_auth_bypass: bool = False
# Нормативы: переопределяют значения по умолчанию из domain/timers.py
timer_limits_ms: dict[TimerCode, int] = {}
def limit_ms(self, code: TimerCode) -> int:
return self.timer_limits_ms.get(code, NORMATIVES[code].limit_ms)
@lru_cache
def get_settings() -> Settings:
return Settings()