# Архитектура Высокоуровневый обзор дизайна системы VidConf. ## Быстрые ссылки - **[frontend-themes.md](./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](./frontend-themes.md)) **Коммуникация:** - HTTP + WebSocket → FastAPI backend (`/api/v1/*`) - WebRTC → LiveKit SFU Подробнее: [frontend/README.md](../../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](../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](../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](../../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](../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) ```sql INSERT INTO conferences (scheduled_at, duration_minutes) VALUES ('2026-07-15T14:30:00+00:00', 60); ``` ### Client-side (JavaScript) ```typescript const startUtc = new Date('2026-07-15T14:30:00Z'); // Браузер форматирует в локальный часовой пояс пользователя ``` ### .ics export ```ics 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/](./adr/): - [001-dynamic-conferences-pivot.md](./adr/001-dynamic-conferences-pivot.md) — динамические конференции вместо бронирований - [002-phrase-attribution-session-participant.md](./adr/002-phrase-attribution-session-participant.md) — атрибуция фраз и треков к участнику сеанса - [003-conference-invitees.md](./adr/003-conference-invitees.md) — приглашённые на конференцию - [004-ai-tier-matrix.md](./adr/004-ai-tier-matrix.md) — матрица уровней AI - [005-password-reset-deferred.md](./adr/005-password-reset-deferred.md) — сброс пароля по email отложен - [000-template.md](./adr/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](../deploy/hardware-profiles.md) и [ADR-004](./adr/004-ai-tier-matrix.md). --- ## Развёртывание ### Установка ```bash ./install.sh # интерактивный опросник (автодетект железа) ./install.sh --preset 3 # неинтерактивно (пресет 3 = AI min) ``` Инсталлятор автоматически: 1. Детектирует CPU/RAM/GPU 2. Рекомендует пресет 3. Генерирует `.env` с секретами 4. Запускает `docker compose` с нужными профилями 5. Выполняет миграции и seed ### Локальная разработка (вручную) ```bash docker compose -f deploy/docker-compose.yml up -d # пресет 1 (без AI) docker compose -f deploy/docker-compose.yml \ --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](../deploy/dev-setup.md). --- ## Мониторинг и наблюдаемость - **Логи:** Docker-логи + логирование приложений - **Метрики:** `GET /metrics` — `vidconf_http_request_duration_seconds`, `vidconf_pipeline_sessions`, `vidconf_celery_queue_depth` (см. [workers/README.md](../../workers/README.md#метрики)) - **Визуализация:** Grafana (compose-профиль `monitoring`) - **БД:** журнал медленных запросов PostgreSQL См. [docs/deploy/monitoring.md](../deploy/monitoring.md). --- ## Стратегия тестирования - **Backend:** pytest (unit + интеграционные тесты) - **Frontend:** ESLint + TypeScript (`tsc -b`); автоматических unit/E2E тестов пока нет - **БД:** проверка версии Alembic (`alembic current`), автогенерация миграций --- ## Ссылки - [Корневой README](../../README.md) - [Схема БД](../db/schema.md) - [Справка API](../api/README.md) - [Архитектура плагинов](../plugins/contracts.md) - [Профили развёртывания](../deploy/hardware-profiles.md) - [Backend README](../../backend/README.md) - [Frontend README](../../frontend/README.md) - [Workers README](../../workers/README.md)