docker compose определяет .env для подстановки ${VAR} по каталогу
compose-файла (deploy/), а не по текущей директории — repo-root .env,
который использует install.sh и вся документация, молча не подхватывался.
Это и была причина "WARN: LIVEKIT_API_KEY not set" на боевом сервере:
секреты были в .env, но compose их не видел и подставлял небезопасные
дефолты (см. таблицу разведки сервера).
Теперь ставшие обязательными ${VAR:?} в docker-compose.yml (см. предыдущий
коммит) без этого немедленно проваливали бы конфиг на любой из
документированных команд. Добавлен `--env-file .env`/"$ENV_FILE" ко всем
вызовам docker compose в install.sh и в командах из README/docs.
install.sh дополнительно: ensure_default (аналог ensure_secret без генерации
секрета) для новых не-секретных параметров nginx/coturn/livekit
(NGINX_SERVER_NAMES, NGINX_CERT_NAME, LIVEKIT_USE_EXTERNAL_IP,
LIVEKIT_NODE_IP, TURN_EXTERNAL_IP) — дефолты только для локальной
разработки, не перезаписывают значения, заданные вручную на боевом
сервере. Плюс вызов deploy/render-templates.sh перед сборкой/подъёмом
стека.
32 KiB
Архитектура
Высокоуровневый обзор дизайна системы VidConf.
Быстрые ссылки
- frontend-themes.md — архитектура темы оболочки: механизм
data-theme, localStorage, React-hook, анти-FOUC, инвариант комнаты.
Архитектура системы
┌─────────────────────────────────────────────────────────────┐
│ Web Browsers │
│ (React SPA + LiveKit JS SDK) │
└──────────────────────────┬──────────────────────────────────┘
│ HTTPS
┌──────▼───────┐
│ Nginx │ (TLS termination)
│ Reverse │
│ Proxy │
└──────┬───────┘
┌──────────────────┼──────────────────┐
│ │ │
┌────▼────┐ ┌──────▼──────┐ ┌─────▼─────┐
│ FastAPI │ │ LiveKit │ │ Coturn │
│ Backend │ │ SFU │ │ TURN │
└────┬────┘ └──────┬──────┘ └───────────┘
│ │
│ (TCP/Postgres) │ (RTP/SRTP WebRTC)
┌────▼──────────────────┘
│
├──► PostgreSQL 16
│ - 14 таблиц: users, teams, conferences, conference_sessions и т.д.
│
├──► Redis
│ - Celery broker
│ - Rate limiting, refresh-токены, pub/sub чата
│
└──► Celery воркеры (раздельные очереди)
├─► transcription (faster-whisper small/medium/large-v3 → фразы)
├─► summarize (Qwen3.5 4B/9B/35B-A3B → резюме, 3 уровня)
└─► notify (email + .ics → SMTP)
Основные компоненты
1. Frontend (React SPA)
Location: frontend/
Tech: React 18 + TypeScript + Vite, Tailwind CSS 4 + @tailwindcss/vite, shadcn/ui, @livekit/components-react, FullCalendar
Функциональность:
- Аутентификация (вход, регистрация, верификация email), JWT (access — заголовок Authorization, refresh — httpOnly cookie)
- Лобби-хаб, календарь (создание/редактирование/отмена конференций, повторение), «мои конференции»
- Вход в конференцию по номеру/ссылке, гостевой вход
- Комната конференции: видео/аудио через LiveKit JS SDK, чат в реальном времени, настройки устройств, fullscreen, Picture-in-Picture, демонстрация экрана
- Профиль пользователя (ФИО, команда, аватар, смена пароля)
- Админ-панель (конференции, пользователи, команды, настройки инстанса)
- Светлая/тёмная тема оболочки (см. frontend-themes.md)
Коммуникация:
- HTTP + WebSocket → FastAPI backend (
/api/v1/*) - WebRTC → LiveKit SFU
Подробнее: frontend/README.md.
2. Backend (FastAPI)
Location: backend/
Tech: Python 3.12, FastAPI, SQLAlchemy 2.0 async, Alembic
Реализовано:
- Схема БД + миграции (14 таблиц + расширение
btree_gist, установлено, но не используется текущими constraint'ами) - Контракты плагинов (Transcriber, Summarizer) + factory с реестром (
@register_transcriber,@register_summarizer) NullTranscriber,NullSummarizer(no-op),FasterWhisperCPU/FasterWhisperGPU,QwenLocal- JWT аутентификация (вход, регистрация, верификация email, refresh, logout), OAuth2 password flow
- Генерация LiveKit токенов, приём и обработка webhook-событий LiveKit (room_started, participant_joined/left, room_finished)
- Динамические конференции: создание (мгновенное/плановое), календарь, вход по номеру/ссылке/паролю, гостевой вход, повторение
- Приглашения на конференцию (пользователь или внешний email), рассылка .ics
- Чат конференции (WebSocket, история, broadcast через Redis pub/sub)
- Администрирование: конференции, пользователи, команды, настройки инстанса
- Endpoint
GET /api/health(проверка БД/Redis),GET /metrics(Prometheus)
API структура:
/api/v1/
├── auth/ register, verify-email, token, refresh, logout, registration-options
├── users/ me (GET/PATCH), me/avatar (POST/DELETE), me/password, поиск (GET ?q=)
├── teams/ справочник команд (GET)
├── conferences/ POST create, GET /my, GET /calendar, GET /resolve,
│ POST /{id}/join, POST /{id}/guest-join, GET/PATCH/DELETE /{id}
├── conferences/{id}/chat WebSocket-чат
├── admin/ conferences, users, teams, settings
└── livekit/ webhook приёмник
/api/health статус системы (БД, Redis)
/metrics метрики Prometheus
Полная спецификация: docs/api/README.md.
3. Database (PostgreSQL 16)
Location: миграции в backend/alembic/versions/ (9 миграций)
Основные таблицы:
| Таблица | Назначение |
|---|---|
users |
Зарегистрированные пользователи (email, password_hash, role, is_blocked, team_id, avatar_path) |
teams |
Справочник команд |
email_verification_tokens |
Одноразовые токены верификации email |
conferences |
Постоянные сущности конференций (номер, slug, владелец, статус, recurrence, summary_recipients, ics_sequence) |
conference_invitees |
Приглашённые на конференцию (user_id ИЛИ email) |
guest_access |
Гости, представившиеся при входе (display_name, email) |
conference_sessions |
Один запуск конференции — единица AI-пайплайна (t_start, t_end, pipeline_status, summary_data) |
conference_participants |
Отслеживание участия в сеансе (user_id ИЛИ guest_id, session_id, joined_at, left_at) |
session_audio_tracks |
Аудиотреки сеанса (per-track запись, статус транскрибации, segments JSONB) |
phrases |
Текстовые сегменты транскрибации (participant_id, session_id, t_start, t_end) |
chat_messages |
Сообщения в сеансе (session_id, автор — user_id или guest_access_id, author_name) |
email_deliveries |
Журнал отправленных писем (саммари, приглашения) — идемпотентность рассылки |
instance_settings |
Настройки инстанса (key-value JSONB): AI-уровень, чат, таймзона, режим рассылки, опции регистрации |
livekit_webhook_events |
Журнал webhook-событий для идемпотентности (event_id, event_type, received_at) |
Ключевые особенности:
- UUID первичные ключи (кроме
phrases/chat_messages→ BIGINT Identity) - Все времена в UTC (
timestamptz) - Номер (9 цифр) и slug (base64url) конференции — публичные идентификаторы доступа
- Recurrence как JSONB (не RRULE; 4 типа: weekly/biweekly/monthly/every_n_days)
- Гости как полноценные участники (
guest_idвconference_participants)
См. docs/db/schema.md — полная ER-диаграмма и обоснования дизайна.
4. LiveKit SFU
Profile compose: media
Tech: LiveKit (Go-based SFU)
Роль:
- SFU (Selective Forwarding Unit) для WebRTC
- Per-track аудио (один трек = один спикер, исключает необходимость диаризации)
- Запись треков через Egress (
.ogg, opus) для последующей транскрибации - FastAPI генерирует LiveKit-токены (JWT, TTL 6 часов, per-room), браузер подключается через LiveKit JS SDK
5. Celery Workers
Location: workers/
Tech: Celery + Redis (message broker), раздельные очереди (transcription/summarize/notify)
Задачи:
| Задача | Вход | Процесс | Выход |
|---|---|---|---|
run_pipeline() |
session_id | Диспетчер + транскрибация (faster-whisper) + реконструкция фраз | session_audio_tracks.segments, phrases |
summarize_session() |
session_id | Map-reduce Qwen3.5 (уровень из instance_settings) | conference_sessions.summary_data |
notify_session() |
session_id | Генерация email + .ics, рассылка по режиму (all/owner) | email_deliveries |
send_invitations() |
conference_id, emails | Генерация .ics, рассылка приглашений | email_deliveries |
cleanup_conferences() (beat) |
— | Закрытие зависших сеансов, завершение просроченных конференций | — |
recover_stuck_summaries() / recover_stuck_notifications() (beat) |
— | Переставить зависшую summarize_session/notify_session в очередь |
завершение пайплайна |
State machine pipeline_status:
recording (создана при t_start сеанса, LiveKit Egress записывает)
↓ (webhook room_finished → enqueue run_pipeline в очередь transcription)
transcribing (транскрибация faster-whisper + реконструкция фраз)
↓
summarizing (map-reduce Qwen3.5 → очередь summarize; summary_data записан, статус не меняется)
↓ (notify_session отправляет уведомления → очередь notify)
notified
↓ Успех: история сохранена в БД
↘ (при ошибке на любом шаге)
↘ failed
↘ Лог; beat-задачи recover_stuck_* могут переставить зависший шаг
Переход в notified:
- Собрать получателей по эффективному режиму рассылки (
conference.summary_recipients or cfg.summary_recipients) - Режим
'all'→ участники сеанса (users) + гости с email - Режим
'owner'→ email владельца конференции - Отправить письмо для каждого нового адреса (таблица
email_deliveriesобеспечивает идемпотентность) - Переход в
notifiedнезависимо от того, были ли получатели
Идемпотентность: каждый шаг проверяет pipeline_status перед началом и пропускает уже пройденные шаги.
Подробнее: workers/README.md.
6. AI Plugins (Strategy Pattern)
Location: backend/core/plugins/
Interfaces:
Transcriber.transcribe(audio_path, language) → list[Segment]Summarizer.summarize(transcript) → str
Factory:
- Registry:
@register_transcriber,@register_summarizer - Configuration:
config/plugins.yaml - No-op defaults:
NullTranscriber,NullSummarizer
Расширяемость:
- Добавить нового провайдера = новый класс + строка в конфиге
- Никаких изменений в основном коде
См. docs/plugins/contracts.md.
7. Nginx Reverse Proxy
Конфиг: deploy/nginx.conf
Задачи:
- Завершение TLS
- Обслуживание статических файлов (frontend SPA)
- Маршрутизация запросов API → FastAPI
- Проксирование WebSocket (чат конференции)
- Сжатие Gzip
Поток данных
1. Пользователь входит в конференцию
Browser
│ 1. POST /api/v1/conferences/{id}/join (пароль опционально)
├─────────────────────────────────► FastAPI
│ │
│ 2. Проверка доступа, генерация LiveKit-токена
│ │
│ 3. {"token": "...", "url": "...", "chat_enabled": true}
◄─────────────────────────────────────┤
│
4. Подключение через LiveKit JS SDK
├────────────────► LiveKit SFU
│ │
│ 5. Запись аудио per-track через Egress (при завершении сеанса)
└ (video/audio stream) ──────────►
БД: INSERT conference_participants (user_id ИЛИ guest_id, session_id, joined_at).
2. Конференция заканчивается → пост-обработка
room_finished webhook
│
1. Backend помечает завершение записи, ожидает завершения egress
│
2. enqueue_pipeline(session_id) → Redis, очередь `transcription`
│
3. run_pipeline(session_id) — диспетчер пайплайна
├─► Ожидание завершения egress (retry с backoff)
├─► Для каждого трека:
│ ├─► Загрузить плагин transcriber (FasterWhisperCPU/GPU или null)
│ ├─► transcriber.transcribe(file_path, language='ru')
│ └─► Сохранить segments в session_audio_tracks.segments (JSONB)
├─► Собрать сегменты по участникам (с учётом смещений)
├─► build_phrases(segments_by_participant, track_offsets)
├─► DELETE phrases + INSERT новые → таблица phrases
├─► UPDATE pipeline_status = 'summarizing'
│
4. send_task_with_retry ставит summarize_session → очередь `summarize`
│
5. summarize_session
├─► Получить фразы сеанса, собрать транскрипт
├─► Разбить на чанки (20 мин по умолчанию, зависит от уровня AI)
├─► Qwen3.5 map-reduce (промпты в workers/summarizer/prompts/)
├─► UPDATE conference_sessions.summary_data = '{...}' (pipeline_status остаётся 'summarizing')
│
6. send_task_with_retry ставит notify_session → очередь `notify`
│
7. notify_session
├─► Собрать получателей (режим: all=участники+гости / owner=владелец)
├─► Для каждого получателя: проверить email_deliveries, отправить письмо
├─► Сгенерировать HTML + plaintext письмо (backend/services/email_templates.py)
├─► Отправить через email-бэкенд (console|smtp, backend/services/email.py)
├─► UPDATE email_deliveries (идемпотентность повторной отправки)
├─► UPDATE pipeline_status = 'notified' (когда все получатели обработаны)
│
8. Beat-задачи recover_stuck_summaries/recover_stuck_notifications (интервал 300 сек)
├─► Найти сеансы, зависшие в 'summarizing' (без summary_data или без уведомления)
├─► Переставить соответствующую задачу в очередь для восстановления после сбоя брокера
Ключевые особенности:
- Диспетчер единый:
run_pipeline()управляет всеми шагами транскрибации - Per-track идемпотентность: каждый трек коммитится отдельно (точка возобновления)
- Реконструкция фраз: группировка по спикеру, короткие вставки, перекрытия речи (
workers/transcription/phrases.py) - Атрибуция гостям: фразы и треки связаны с
conference_participants, неusers(ADR-002) - Проверка pipeline_status: guard идемпотентности (повторный запуск пропустит готовые шаги)
Обработка времени
Инвариант: все времена в БД в UTC (timestamptz).
Server-side (PostgreSQL)
INSERT INTO conferences (scheduled_at, duration_minutes)
VALUES ('2026-07-15T14:30:00+00:00', 60);
Client-side (JavaScript)
const startUtc = new Date('2026-07-15T14:30:00Z');
// Браузер форматирует в локальный часовой пояс пользователя
.ics export
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//VidConf//EN
BEGIN:VTIMEZONE
TZID:Europe/Moscow
...
END:VTIMEZONE
BEGIN:VEVENT
UID:...
DTSTART;TZID=Europe/Moscow:20260715T143000
DTEND;TZID=Europe/Moscow:20260715T153000
...
END:VEVENT
END:VCALENDAR
Ключевые архитектурные решения
-
Динамические конференции вместо бронирований (ADR-001) — конференция как пользовательская сущность
- Номер (9 цифр) + slug (base64url) — постоянные идентификаторы доступа
- Статусы: scheduled/active/ended
- Повторение: RecurrenceRule (JSON, 4 типа)
- Гости — полноценные участники (
guest_access)
-
Номер и slug как идентификаторы доступа (ADR-001, п.4) — публичные эндпоинты без auth
- Номер: 9 цифр, первая 1..9 (энтропия ≈2^29.75); перебор при rate limit ≈60+ дней
- Slug: base64url 8 байт → 11 символов (64 бита энтропии); имя LiveKit-комнаты
- Резолв: единообразный 404 (живая/мёртвая неразличимы для безопасности)
- Rate limit: 10 запросов в минуту на IP для
/resolveи/guest-join
-
Гости как полноценные участники (ADR-001, п.6) — представление при входе
guest_access: display_name (обязателен), email (факультативен)- LiveKit identity:
guest:{guest_id}(email не передаётся) - Участвуют в пайплайне саммари (email в рассылке, если заполнен)
-
Recurrence как собственная модель, не RRULE (ADR-001, п.3) — 4 типа, привязаны к форме UI
type: weekly | biweekly | monthly | every_n_days- Хранение в JSONB (
conferences.recurrence), развёртка occurrences в памяти - Правило содержит локальное время + IANA-таймзону; развёртка — в UTC
-
Паттерн Strategy для плагинов — подключаемые AI-реализации без изменений ядра
- Интерфейсы Transcriber/Summarizer в
backend/core/plugins/ - Паттерн Factory с реестром
- Конфиг через YAML
- Интерфейсы Transcriber/Summarizer в
-
Идемпотентный pipeline пост-обработки — безопасно повторять любой шаг
- Каждый шаг проверяет
pipeline_statusперед продолжением - Per-track коммит сегментов (точка возобновления в БД)
- Неудачные задачи можно повторить без дублирования (DELETE+INSERT фраз в одной транзакции)
- Каждый шаг проверяет
-
Атрибуция фраз и треков к участнику сеанса (ADR-002) — поддержка гостей без user_id
phrases.participant_id→ FKconference_participants.id(вместо user_id)session_audio_tracks.participant_id→ FKconference_participants.id- Гость без
user_idпроходит пайплайн наравне с пользователем
-
Per-track аудио в SFU — исключает необходимость диаризации спикеров
- LiveKit предоставляет per-track recording (один трек = один микрофон = один спикер)
- Track SID прямо соответствует identity участника
- Запись через Track Egress в
.ogg(opus)
-
UUID первичные ключи — поддержка распределённых систем и репликации
- Сгенерировано через
gen_random_uuid() - Исключения: таблицы высокой частоты (
phrases,chat_messages) используют BIGINT Identity
- Сгенерировано через
-
Настройки инстанса в БД — бутстрап из
plugins.yaml- Таблица
instance_settings(key-value JSONB) импортирует дефолтыconfig/plugins.yamlпри старте backend - Однократно и идемпотентно (
INSERT ... ON CONFLICT DO NOTHING) - Воркеры читают эффективную конфигурацию на старте каждой задачи
- Административный интерфейс может менять настройки без рестарта
- Таблица
-
Рассылка саммари с переопределением на уровне конференции — гибкая конфигурация
instance_settings.summary_recipients— дефолт инстанса ('all'или'owner')conferences.summary_recipients— переопределение для конкретной конференции (nullable)- Эффективный режим =
conference.summary_recipients or cfg.summary_recipients
-
Идемпотентная рассылка саммари — без дублирования писем
- Таблица
email_deliveriesс уникальным частичным индексом(session_id, recipient_email)WHEREkind='summary' - Повторный запуск уведомления отправляет письмо только адресатам, которых ещё нет в таблице
- Семантика at-least-once: редкий дубль письма возможен, потеря — нет
- Per-получательный commit обеспечивает точку возобновления при обрыве
- Таблица
-
Email-транспорт только через
.env— секреты никогда не попадают в БД или API- Переключатель бэкенда (
EMAIL_BACKEND=console|smtp) — переменная окружения - Все SMTP-реквизиты (хост, порт, пароль, from) — только в
.env - Секреты не логируются и не попадают в
SettingsOutAPI aiosmtplibс обработкой ошибок (retryable vs. skip)
- Переключатель бэкенда (
-
Управление командами и опциями регистрации — справочник команд и гибкий вход
- Таблица
teams— справочник команд (id, name UNIQUE, created_at); админ-API CRUD users.team_id(nullable) → FKteams.idсON DELETE SET NULL- Настройка инстанса
registration_team_choice(bool) — показ выбора команды при регистрации;GET /auth/registration-optionsвозвращает список доступных команд - Настройка инстанса
registration_email_domain(bool, domain: str|null) — обязательное совпадение домена email при регистрации (иначе 400invalid_email_domain)
- Таблица
Безопасность
- Пароли: хэширование Argon2 (никогда не логируется и не раскрывается)
- JWT access-токен: HS256, TTL по умолчанию 15 минут (
ACCESS_TOKEN_TTL_MINUTES), передаётся в заголовкеAuthorization: Bearer - Refresh-токен: httpOnly SameSite=Strict cookie, TTL по умолчанию 14 дней; отзывается при logout
- LiveKit токены: ограничены по времени (TTL 6 часов), выдаются на конкретную комнату
- HTTPS/TLS: терминируется на Nginx; dev-окружение использует HTTP
- CSRF: митигируется
SameSite=Strictна cookie с refresh-токеном (не отправляется с cross-site запросов); access-токен передаётся в заголовке, а не в cookie, и потому не подвержен CSRF - SQL injection: SQLAlchemy ORM с параметризованными запросами
- Rate limiting: Redis-based, публичные эндпоинты (
/resolve,/guest-join) — 10 запросов/мин на IP
ADR (Записи архитектурных решений)
Подробные обоснования см. в папке adr/:
- 001-dynamic-conferences-pivot.md — динамические конференции вместо бронирований
- 002-phrase-attribution-session-participant.md — атрибуция фраз и треков к участнику сеанса
- 003-conference-invitees.md — приглашённые на конференцию
- 004-ai-tier-matrix.md — матрица уровней AI
- 005-password-reset-deferred.md — сброс пароля по email отложен
- 000-template.md — шаблон ADR
Производительность и масштабируемость
Уровни AI качества (ADR-004)
Пресет 1–2 (без AI):
- 10–20 одновременных конференций
- 4 vCPU, 8 GB RAM, 40 GB диск
Пресет 3: AI min (faster-whisper small + Qwen3.5-4B)
- 10–20 одновременных конференций
- 8 vCPU, 16 GB RAM, 100 GB диск
- Транскрибация: ~2x realtime (10мин → 5мин на 8-ядерном CPU)
Пресет 4: AI medium (faster-whisper medium + Qwen3.5-9B; GPU опционально)
- 15–30 одновременных конференций (CPU) или 30–50 (GPU)
- 12–16 vCPU, 32 GB RAM, 150 GB диск
- CPU: ~3x realtime; GPU (≥8 GB): ~1.5x realtime
Пресет 5: AI max (faster-whisper large-v3 + Qwen3.5-35B-A3B; GPU обязателен)
- 50–100+ одновременных конференций
- 16+ vCPU, 64 GB RAM, 250 GB диск, GPU ≥16 GB VRAM
- Транскрибация: ~0.5x realtime (10мин → 20сек на V100)
См. docs/deploy/hardware-profiles.md и ADR-004.
Развёртывание
Установка
./install.sh # интерактивный опросник (автодетект железа)
./install.sh --preset 3 # неинтерактивно (пресет 3 = AI min)
Инсталлятор автоматически:
- Детектирует CPU/RAM/GPU
- Рекомендует пресет
- Генерирует
.envс секретами - Запускает
docker composeс нужными профилями - Выполняет миграции и seed
Локальная разработка (вручную)
docker compose -f deploy/docker-compose.yml --env-file .env up -d # пресет 1 (без AI)
docker compose -f deploy/docker-compose.yml --env-file .env \
--profile media --profile transcribe --profile llm up -d # пресет 3 (с AI min)
Продакшен
- Docker Compose на одном хосте (текущий целевой сценарий)
- Nginx для TLS + статические файлы
- Переменные окружения для секретов (
.env) - Резервные копии БД (PostgreSQL dump)
- Мониторинг (Prometheus + Grafana,
--profile monitoring)
Мониторинг и наблюдаемость
- Логи: Docker-логи + логирование приложений
- Метрики:
GET /metrics—vidconf_http_request_duration_seconds,vidconf_pipeline_sessions,vidconf_celery_queue_depth(см. workers/README.md) - Визуализация: Grafana (compose-профиль
monitoring) - БД: журнал медленных запросов PostgreSQL
См. docs/deploy/monitoring.md.
Стратегия тестирования
- Backend: pytest (unit + интеграционные тесты)
- Frontend: ESLint + TypeScript (
tsc -b); автоматических unit/E2E тестов пока нет - БД: проверка версии Alembic (
alembic current), автогенерация миграций