Files
Max Ronzhin 693db6e774 install.sh, docs: pass --env-file explicitly to docker compose
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 перед сборкой/подъёмом
стека.
2026-07-25 21:58:03 +03:00
..

Архитектура

Высокоуровневый обзор дизайна системы 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

Ключевые архитектурные решения

  1. Динамические конференции вместо бронирований (ADR-001) — конференция как пользовательская сущность

    • Номер (9 цифр) + slug (base64url) — постоянные идентификаторы доступа
    • Статусы: scheduled/active/ended
    • Повторение: RecurrenceRule (JSON, 4 типа)
    • Гости — полноценные участники (guest_access)
  2. Номер и 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
  3. Гости как полноценные участники (ADR-001, п.6) — представление при входе

    • guest_access: display_name (обязателен), email (факультативен)
    • LiveKit identity: guest:{guest_id} (email не передаётся)
    • Участвуют в пайплайне саммари (email в рассылке, если заполнен)
  4. Recurrence как собственная модель, не RRULE (ADR-001, п.3) — 4 типа, привязаны к форме UI

    • type: weekly | biweekly | monthly | every_n_days
    • Хранение в JSONB (conferences.recurrence), развёртка occurrences в памяти
    • Правило содержит локальное время + IANA-таймзону; развёртка — в UTC
  5. Паттерн Strategy для плагинов — подключаемые AI-реализации без изменений ядра

    • Интерфейсы Transcriber/Summarizer в backend/core/plugins/
    • Паттерн Factory с реестром
    • Конфиг через YAML
  6. Идемпотентный pipeline пост-обработки — безопасно повторять любой шаг

    • Каждый шаг проверяет pipeline_status перед продолжением
    • Per-track коммит сегментов (точка возобновления в БД)
    • Неудачные задачи можно повторить без дублирования (DELETE+INSERT фраз в одной транзакции)
  7. Атрибуция фраз и треков к участнику сеанса (ADR-002) — поддержка гостей без user_id

    • phrases.participant_id → FK conference_participants.id (вместо user_id)
    • session_audio_tracks.participant_id → FK conference_participants.id
    • Гость без user_id проходит пайплайн наравне с пользователем
  8. Per-track аудио в SFU — исключает необходимость диаризации спикеров

    • LiveKit предоставляет per-track recording (один трек = один микрофон = один спикер)
    • Track SID прямо соответствует identity участника
    • Запись через Track Egress в .ogg (opus)
  9. UUID первичные ключи — поддержка распределённых систем и репликации

    • Сгенерировано через gen_random_uuid()
    • Исключения: таблицы высокой частоты (phrases, chat_messages) используют BIGINT Identity
  10. Настройки инстанса в БД — бутстрап из plugins.yaml

    • Таблица instance_settings (key-value JSONB) импортирует дефолты config/plugins.yaml при старте backend
    • Однократно и идемпотентно (INSERT ... ON CONFLICT DO NOTHING)
    • Воркеры читают эффективную конфигурацию на старте каждой задачи
    • Административный интерфейс может менять настройки без рестарта
  11. Рассылка саммари с переопределением на уровне конференции — гибкая конфигурация

    • instance_settings.summary_recipients — дефолт инстанса ('all' или 'owner')
    • conferences.summary_recipients — переопределение для конкретной конференции (nullable)
    • Эффективный режим = conference.summary_recipients or cfg.summary_recipients
  12. Идемпотентная рассылка саммари — без дублирования писем

    • Таблица email_deliveries с уникальным частичным индексом (session_id, recipient_email) WHERE kind='summary'
    • Повторный запуск уведомления отправляет письмо только адресатам, которых ещё нет в таблице
    • Семантика at-least-once: редкий дубль письма возможен, потеря — нет
    • Per-получательный commit обеспечивает точку возобновления при обрыве
  13. Email-транспорт только через .env — секреты никогда не попадают в БД или API

    • Переключатель бэкенда (EMAIL_BACKEND=console|smtp) — переменная окружения
    • Все SMTP-реквизиты (хост, порт, пароль, from) — только в .env
    • Секреты не логируются и не попадают в SettingsOut API
    • aiosmtplib с обработкой ошибок (retryable vs. skip)
  14. Управление командами и опциями регистрации — справочник команд и гибкий вход

    • Таблица teams — справочник команд (id, name UNIQUE, created_at); админ-API CRUD
    • users.team_id (nullable) → FK teams.id с ON DELETE SET NULL
    • Настройка инстанса registration_team_choice (bool) — показ выбора команды при регистрации; GET /auth/registration-options возвращает список доступных команд
    • Настройка инстанса registration_email_domain (bool, domain: str|null) — обязательное совпадение домена email при регистрации (иначе 400 invalid_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/:


Производительность и масштабируемость

Уровни AI качества (ADR-004)

Пресет 12 (без AI):

  • 1020 одновременных конференций
  • 4 vCPU, 8 GB RAM, 40 GB диск

Пресет 3: AI min (faster-whisper small + Qwen3.5-4B)

  • 1020 одновременных конференций
  • 8 vCPU, 16 GB RAM, 100 GB диск
  • Транскрибация: ~2x realtime (10мин → 5мин на 8-ядерном CPU)

Пресет 4: AI medium (faster-whisper medium + Qwen3.5-9B; GPU опционально)

  • 1530 одновременных конференций (CPU) или 3050 (GPU)
  • 1216 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 обязателен)

  • 50100+ одновременных конференций
  • 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)

Инсталлятор автоматически:

  1. Детектирует CPU/RAM/GPU
  2. Рекомендует пресет
  3. Генерирует .env с секретами
  4. Запускает docker compose с нужными профилями
  5. Выполняет миграции и 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)

См. docs/deploy/dev-setup.md.


Мониторинг и наблюдаемость

  • Логи: Docker-логи + логирование приложений
  • Метрики: GET /metricsvidconf_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), автогенерация миграций

Ссылки