Первоначальная версия VidConf
This commit is contained in:
0
docs/deploy/.gitkeep
Normal file
0
docs/deploy/.gitkeep
Normal file
268
docs/deploy/capacity.md
Normal file
268
docs/deploy/capacity.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# Нагрузочное тестирование SFU (LiveKit): методика и ёмкость
|
||||
|
||||
Оценивает,
|
||||
сколько одновременных издателей аудио+видео и подписчиков выдерживает
|
||||
LiveKit SFU в текущей конфигурации compose (`deploy/livekit/livekit.yaml`),
|
||||
и даёт формулу прикидки ёмкости для прод-железа из таблицы пресетов
|
||||
ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`).
|
||||
|
||||
## ВАЖНО: окружение — это dev-Mac, не прод-референс
|
||||
|
||||
Все цифры ниже получены на разработческом macOS-хосте (Apple Silicon,
|
||||
10 физических ядер CPU, 16 ГБ RAM), где Docker Desktop выделяет под
|
||||
контейнеры лёгкую VM: **10 vCPU / 7.75 ГБ RAM** (`docker info`). Это НЕ
|
||||
прод-железо и НЕ Linux-хост — абсолютные цифры (число участников,
|
||||
момент деградации) **нельзя** переносить на прод напрямую. Причины:
|
||||
|
||||
1. Docker Desktop на macOS обрабатывает часть сетевого стека (в первую
|
||||
очередь высокочастотный UDP-трафик WebRTC) вне cgroup контейнера —
|
||||
в собственном сетевом прокси VM (`vpnkit`/`gvisor-tap-vsock`). Эта
|
||||
нагрузка **не видна** в `docker stats` (CPU% измеряется только внутри
|
||||
контейнера `livekit`), но реально потребляет ресурсы хоста. На Linux
|
||||
(прод, bare-metal или облачная VM с host-networking) этого прокси-слоя
|
||||
нет — CPU-профиль SFU там будет заметно легче при той же нагрузке.
|
||||
2. На хосте параллельно во время части прогонов выполнялась одна и та же
|
||||
Docker VM с другими контейнерами разработческого стека (LLM-сервер
|
||||
`llm`, Celery `worker`) — при их одновременной активности они отъедали
|
||||
до 4–9 vCPU из тех же 10 (см. раздел «Артефакт: конкуренция за CPU» —
|
||||
первый прогон 20×20 был контаминирован, повторён после `docker stop
|
||||
llm worker`).
|
||||
3. Диапазон UDP-портов SFU в dev-compose узкий: `54000-54100` (101 порт,
|
||||
`deploy/docker-compose.yml`, комментарий про конфликты с занятыми
|
||||
портами хоста на macOS) — на проде обычно шире.
|
||||
|
||||
**Вывод:** используйте этот документ для МЕТОДИКИ и ОТНОСИТЕЛЬНОЙ формы
|
||||
кривой деградации (как растут CPU/потери с числом участников), а не как
|
||||
источник абсолютного «сколько человек выдержит прод-сервер». Перед
|
||||
реальным релизом — обязательно повторить те же ступени на целевом
|
||||
Linux-хосте (см. `docs/architecture/adr/004-ai-tier-matrix.md`, таблица
|
||||
требований по пресетам).
|
||||
|
||||
## Методика
|
||||
|
||||
### Инструмент
|
||||
|
||||
`lk load-test` из `livekit-cli` (проверено через find-docs,
|
||||
github.com/livekit/livekit-cli README, актуальная версия **2.18.0**,
|
||||
`brew install livekit-cli`). Команда и параметры:
|
||||
|
||||
```bash
|
||||
lk load-test \
|
||||
--room <имя> --duration <N>s \
|
||||
--video-publishers <N> --audio-publishers <N> --subscribers <M> \
|
||||
--layout 5x5 --num-per-second 30
|
||||
```
|
||||
|
||||
Ключевые флаги:
|
||||
- `--video-publishers` / `--audio-publishers` — при **равном** числе оба
|
||||
флага применяются к ОДНОМУ и тому же набору тестовых identity (не
|
||||
удваивают число участников): N издателей публикуют по одному
|
||||
аудио- и видеотреку каждый (видео — с simulcast, 3 слоя `q`/`h`/`f` —
|
||||
quarter/half/full, включён по умолчанию, флаг `--no-simulcast`
|
||||
отключает).
|
||||
- `--subscribers` — M подписчиков, каждый подписывается на все треки,
|
||||
доступные в комнате НА МОМЕНТ его подключения.
|
||||
- `--layout` (`speaker`/`3x3`/`4x4`/`5x5`, по умолчанию `speaker`) —
|
||||
**важно**: это не жёсткий лимит числа подписок, а имитация того, какое
|
||||
разрешение (какой simulcast-слой) подписчик запрашивает под сетку
|
||||
такого размера. Дефолтный `speaker` в тестах ограничивал реальное число
|
||||
подписок (см. «Находка» ниже) — для честного all-to-all fan-out
|
||||
используйте `5x5` (или `4x4`/`3x3` под нужный размер комнаты).
|
||||
- `--num-per-second` (по умолчанию 5) — темп присоединения тестовых
|
||||
участников; при большом числе участников и низком значении подписчики
|
||||
успевают подключиться раньше, чем опубликуются все треки, и видят
|
||||
усечённый набор — не итог деградации SFU, а гонка старта теста. В
|
||||
прогонах ниже установлен `30`.
|
||||
|
||||
### Находка 1: `--layout speaker` — не «весь мэш»
|
||||
|
||||
Первый прогон 10×10 с дефолтным layout дал устойчиво 12/20 треков на
|
||||
подписчика (не деградация, а имитация UI «спикер + несколько миниатюр» —
|
||||
подписчик просто не запрашивает все треки комнаты). Для честной оценки
|
||||
верхней границы ёмкости SFU (все видят всех — pessimistic case) использован
|
||||
`--layout 5x5` (до 25 видимых плиток) во всех ступенях ниже.
|
||||
|
||||
### Находка 2: `lk load-test` требует TURN/ICE-сервер, доступный БЫСТРО
|
||||
|
||||
Со штатным dev-конфигом `deploy/livekit/livekit.yaml` (`turn.enabled: false`,
|
||||
`rtc.turn_servers` не задан) LiveKit отдаёт клиенту **пустой** список ICE-
|
||||
серверов. При пустом списке клиентский SDK (`livekit-cli`/pion) молча
|
||||
подставляет свой дефолтный публичный STUN-список
|
||||
(`stun:global.stun.twilio.com`, `stun:stun.l.google.com`,
|
||||
`stun:stun1.l.google.com`). В песочнице этого запуска исходящий путь к этим
|
||||
публичным STUN-серверам идёт через виртуальную сеть Docker Desktop с
|
||||
заметной задержкой (наблюдались ICE-соединения за 5–7 с), а у
|
||||
`lk load-test` **фиксированный** `ConnectTimeout: 5s` — часть подключений
|
||||
(в первую очередь PUBLISHER-транспорт) не укладывалась и обрывалась с
|
||||
`could not connect after timeout`, даже когда сама SFU была не при делах.
|
||||
|
||||
Обход для теста: временно включить встроенный TURN-сервер LiveKit
|
||||
(`turn.enabled: true`, `udp_port: 3478`, `tls_port: 0` — без TLS-варианта,
|
||||
т.к. сертификат не нужен для локального UDP-теста) через ВРЕМЕННЫЙ
|
||||
compose-override (не коммитился, не трогает `deploy/livekit/livekit.yaml`).
|
||||
Тогда сервер начинает отдавать клиенту непустой список ICE-серверов
|
||||
(собственный TURN вместо пустого списка), клиентский SDK не откатывается на
|
||||
публичные STUN, и ICE устанавливается за <1.5 с.
|
||||
|
||||
**Рекомендация для прода** (не реализована в рамках этой задачи — вне
|
||||
скоупа нагрузочного теста, требует правки `deploy/livekit/livekit.yaml`
|
||||
отдельным изменением): зарегистрировать существующий standalone-coturn
|
||||
(`deploy/coturn/`) в `rtc.turn_servers` LiveKit, чтобы клиенты ВСЕГДА
|
||||
получали явный ICE-сервер и не откатывались на публичные Google/Twilio
|
||||
STUN — это одновременно (а) быстрее устанавливает соединение за NAT и
|
||||
(б) не отдаёт IP участников третьим сторонам без необходимости (тот же дух
|
||||
приватности данных, что и требование локальных AI-моделей — идея та же).
|
||||
|
||||
### Ступени
|
||||
|
||||
Ступени росли до чёткой деградации: 5×5 → 10×10 → 20×20 → 30×30 (число
|
||||
издателей аудио+видео × число подписчиков, оба со флагом `--layout 5x5`,
|
||||
`--num-per-second 30`, длительность 45–55 c на ступень). Параллельно
|
||||
каждые 2 с снимался `docker stats --no-stream` по всем контейнерам
|
||||
dev-стека.
|
||||
|
||||
## Результаты по ступеням
|
||||
|
||||
| Ступень | Участников всего | Треков на подписчика (успешно) | Пик CPU `livekit` (ядер) | Пик RAM `livekit` | Суммарный трафик подписчиков | Потери пакетов (агрегат) | Статус |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| 5×5 | 10 | 10/10 | 0.42 | 145 МиБ | 7.0 Мбит/с | 0.016% | OK |
|
||||
| 10×10 | 20 | 20/20 | 1.55 | 226 МиБ | 45.3 Мбит/с | 0.010% | OK |
|
||||
| 20×20 (после устранения конкуренции за CPU, см. ниже) | 40 | 40/40 | 2.42 | 574 МиБ | 176.1 Мбит/с | 0.075% | OK, лёгкий рост |
|
||||
| 30×30 | 60 | 50/60 (9 из 30 подписчиков вообще не подключились) | 6.46 | 815 МиБ | 192.3 Мбит/с (частично) | **2.30%** | **ДЕГРАДАЦИЯ** |
|
||||
|
||||
Промежуточная точка (для полноты): первый прогон 20×20 БЕЗ остановки
|
||||
конкурентных контейнеров `llm`/`worker` (у пользователя в этот момент
|
||||
выполнялась суммаризация/фоновая LLM-нагрузка, до 863% CPU у контейнера
|
||||
`llm` — почти весь бюджет VM) дал 2.111% потерь при том же трафике — то
|
||||
есть выглядел как «деградация на 20×20», хотя на деле это была конкуренция
|
||||
за CPU хоста с посторонним процессом, а не предел самого SFU. После
|
||||
`docker stop vidconf-llm-1 vidconf-worker-1` и повторного прогона потери на
|
||||
20×20 упали до 0.075% (таблица выше — уже чистый прогон). Это
|
||||
задокументировано как явное предупреждение: **на разработческой машине
|
||||
любой параллельный AI-воркер (транскрибация/суммаризация/LLM) искажает
|
||||
результаты SFU-теста** — на проде эти роли обычно разнесены по разным
|
||||
хостам/профилям (ADR-004), поэтому конкуренции не будет, но при тестировании
|
||||
на одной машине (например, пресет 3 «+AI min» на одном сервере) это стоит
|
||||
учитывать при планировании ёмкости.
|
||||
|
||||
## Выводы
|
||||
|
||||
1. **CPU SFU растёт сублинейно, затем резко (колено кривой) —** удельная
|
||||
стоимость ядра на 1000 подписчик-треков падает с 5×5 к 20×20 (амортизация
|
||||
фиксированных издержек процесса), но на 30×30 подскакивает: 6.46 ядра на
|
||||
1050 успешных подписок против 2.42 ядра на 800 на предыдущей ступени —
|
||||
явный признак приближения к пределу однопроцессного узла LiveKit на этой
|
||||
VM. Часть подписчиков (9 из 30) не успели установить соединение за
|
||||
`ConnectTimeout` — при близкой к пределу загрузке CPU ICE/DTLS-хендшейк
|
||||
новых участников начинает конкурировать с уже идущей пересылкой медиа
|
||||
существующих и не укладывается в таймаут.
|
||||
2. **Память не является узким местом** ни на одной ступени (пик 815 МиБ на
|
||||
30×30 при лимите VM 7.75 ГБ) — планировать ёмкость по CPU и сетевой
|
||||
пропускной способности, не по RAM.
|
||||
3. **Битрейты, полученные в тесте** (ориентир для планирования аплинка/
|
||||
даунлинка, реальные величины при `--layout 5x5`, simulcast включён по
|
||||
умолчанию у клиентского SDK LiveKit):
|
||||
- Аудио (Opus): стабильно **~19–21 кбит/с** на трек — использовать
|
||||
20 кбит/с как плановую цифру на одного говорящего участника.
|
||||
- Видео, «сеточный» (не приоритетный) слой simulcast, который SFU
|
||||
форвардит подписчикам при 10+ видимых плитках: **~200–350 кбит/с** на
|
||||
трек — это нижний/средний слой (`q`/`h` в терминах rid). Годится как
|
||||
плановая цифра для комнат с сеткой ≥3×3.
|
||||
- Видео, верхний слой simulcast (форвардится, когда подписчик один/
|
||||
мало плиток, либо трек — «в фокусе»/спикер): **~1.3–2.2 Мбит/с** на
|
||||
трек — плановая цифра для 1:1 звонков и «пришпиленного» видео.
|
||||
- **Рекомендация:** simulcast (3 слоя, дефолт LiveKit JS/Go SDK) уже
|
||||
покрывает оба сценария автоматически — адаптивная подписка (LiveKit
|
||||
`AdaptiveStream`) на фронтенде должна запрашивать нижний слой в
|
||||
сеточных раскладках и верхний — в раскладке «спикер»/pinned, что
|
||||
соответствует наблюдаемому поведению теста. Специальных ручных
|
||||
профилей битрейта заводить не требуется; при необходимости ограничить
|
||||
верхнюю границу — `videoEncoding`/`simulcastLayers` на фронтенде
|
||||
(клиентский SDK, вне скоупа devops-части).
|
||||
4. **UDP-диапазон 54000-54100 (101 порт)** не был узким местом ни на одной
|
||||
ступени (максимум 60 участников в тесте) — при планировании прод-узла с
|
||||
ожидаемым бОльшим числом одновременных участников across все комнаты
|
||||
узла держать `port_range_end - port_range_start` заметно больше пикового
|
||||
числа участников на узле (LiveKit резервирует пару портов на участника
|
||||
на медиа-транспорт).
|
||||
5. **STUN/TURN-находка (см. «Методика») —** рекомендуется отдельной задачей
|
||||
зарегистрировать `deploy/coturn/` в `rtc.turn_servers` LiveKit и на
|
||||
проде, а не только для теста — иначе клиенты в вырожденном случае
|
||||
(или при сбое конфигурации) будут по умолчанию уходить на публичные
|
||||
Google/Twilio STUN.
|
||||
|
||||
## Формула прикидки ёмкости для прод-железа
|
||||
|
||||
Ёмкость SFU для конкретного узла оценивается ПО CPU (наблюдение п.1-2), не
|
||||
по RAM/диску. Использовать данные ступеней **до колена кривой** (5×5,
|
||||
10×10, 20×20 — линейный участок) как основу, оставляя запас до колена, а
|
||||
не экстраполировать до него линейно.
|
||||
|
||||
```
|
||||
vCPU_на_SFU ≈ 0.35 (базовые издержки процесса LiveKit)
|
||||
+ 0.006 × T_total
|
||||
|
||||
где T_total = подписчики × видимых_треков_на_подписчика
|
||||
(видимых_треков = 2 × издателей при полном мэше,
|
||||
или меньше — при layout speaker/3x3/4x4/реальном UI)
|
||||
```
|
||||
|
||||
Коэффициент 0.006 ядра/трек — среднее по трём чистым линейным ступеням
|
||||
(5×5: 0.0084; 10×10: 0.0078; 20×20: 0.0030 — усреднено консервативно в
|
||||
пользу меньшего числа участников, т.к. именно там эффективность на трек
|
||||
ниже). Прикидка ДЛЯ ЭТОГО compose/host-стека; на bare-metal Linux
|
||||
допустимо ожидать меньший коэффициент (нет прокси-сети Docker Desktop) —
|
||||
подтверждать реальным прогоном.
|
||||
|
||||
**Правило безопасного запаса:** держать целевую загрузку `vCPU_на_SFU`
|
||||
**не выше 60% от доступных ядер узла** — в тесте деградация проявилась
|
||||
уже на ~65% формальной квоты VM (6.46 из 10 vCPU), с учётом невидимых
|
||||
docker-stats издержек виртуализации сети реальный физический потолок был
|
||||
ещё ближе. На bare-metal Linux запас может быть меньше, но без отдельной
|
||||
валидации закладывать 60% как консервативный ориентир.
|
||||
|
||||
**Пример применения** к таблице пресетов ADR-004 (`docs/architecture/adr/
|
||||
004-ai-tier-matrix.md`) — эти vCPU общие на весь стек (backend, БД,
|
||||
AI-воркеры и SFU), поэтому реальный бюджет SFU меньше указанного в таблице
|
||||
на объём, потребляемый остальными сервисами:
|
||||
|
||||
| Пресет | vCPU узла | Ориентир бюджета SFU (после вычета backend/БД/AI, груб.) | T_total (при коэф. 0.006 и запасе 60%) | Примерно участников (2 трека/чел., полный мэш) |
|
||||
|---|---|---|---|---|
|
||||
| 1/2 (MVP/+чат, без AI) | 4 | ~3.0 vCPU | ≈ (3.0×0.6−0.35)/0.006 ≈ 240 | ≈ 120 |
|
||||
| 3 (+AI min) | 8 | ~4.0 vCPU (доля с транскрибацией/суммаризацией) | ≈ (4.0×0.6−0.35)/0.006 ≈ 342 | ≈ 170 |
|
||||
| 4 (+AI medium) | 12–16 | ~5.0 vCPU | ≈ (5.0×0.6−0.35)/0.006 ≈ 458 | ≈ 230 |
|
||||
| 5 (+AI max) | 16+ | ~6.0 vCPU (AI забирает GPU, CPU на SFU свободнее) | ≈ (6.0×0.6−0.35)/0.006 ≈ 575 | ≈ 285 |
|
||||
|
||||
Это цифры участников **на весь узел суммарно по всем одновременным
|
||||
комнатам**, не на одну комнату — при типичных размерах комнат VidConf
|
||||
(рабочие созвоны, не масштабные вебинары) практический потолок числа
|
||||
одновременных КОМНАТ на узел определяется делением на среднее число
|
||||
участников в комнате. Таблица — грубая прикидка для первичного sizing;
|
||||
обязательна проверка реальным `lk load-test` на целевом железе перед
|
||||
production-релизом крупной инсталляции (пресеты 4/5).
|
||||
|
||||
## Как повторить
|
||||
|
||||
```bash
|
||||
# 1. Инструмент
|
||||
brew install livekit-cli # или бинарь с github.com/livekit/livekit-cli/releases
|
||||
|
||||
# 2. Локальный стек (без AI-профилей)
|
||||
docker compose -f deploy/docker-compose.yml --profile media up -d livekit coturn
|
||||
|
||||
# 3. Прогон одной ступени (пример 10×10)
|
||||
export LIVEKIT_URL=ws://localhost:7880
|
||||
export LIVEKIT_API_KEY=devkey
|
||||
export LIVEKIT_API_SECRET=<значение LIVEKIT_API_SECRET из .env>
|
||||
lk load-test --room capacity-10x10 --duration 45s \
|
||||
--video-publishers 10 --audio-publishers 10 --subscribers 10 \
|
||||
--layout 5x5 --num-per-second 30
|
||||
|
||||
# Параллельно в соседнем терминале — снимать нагрузку:
|
||||
watch -n2 'docker stats --no-stream'
|
||||
```
|
||||
|
||||
Для честного измерения ICE-задержки на macOS-хосте с Docker Desktop —
|
||||
см. «Находка 2» выше: либо временно включить `turn.enabled: true` (без TLS,
|
||||
`udp_port: 3478`) через compose-override, либо тестировать на Linux-хосте,
|
||||
где артефакт не проявляется.
|
||||
194
docs/deploy/dev-setup.md
Normal file
194
docs/deploy/dev-setup.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Настройка Dev окружения
|
||||
|
||||
## 1. Требования
|
||||
|
||||
- Docker + Docker Compose v2
|
||||
- `uv` (инструменты backend): `brew install uv`
|
||||
- Node 20+ (frontend)
|
||||
|
||||
## 2. Переменные окружения
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# отредактируйте .env если нужно (defaults работают для локальной разработки)
|
||||
```
|
||||
|
||||
Пример переменных окружения:
|
||||
```env
|
||||
POSTGRES_USER=vidconf
|
||||
POSTGRES_PASSWORD=dev_password
|
||||
POSTGRES_DB=vidconf
|
||||
REDIS_URL=redis://localhost:6379
|
||||
JWT_SECRET=your-secret-key-here
|
||||
SEED_ADMIN_EMAIL=admin@example.com
|
||||
SEED_ADMIN_PASSWORD=admin123
|
||||
```
|
||||
|
||||
## 3. Запуск базового стека (без видеоконференций)
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
docker compose -f deploy/docker-compose.yml ps
|
||||
```
|
||||
|
||||
Это запустит:
|
||||
- `postgres` — База данных PostgreSQL
|
||||
- `redis` — Redis (очередь Celery)
|
||||
- `backend` — FastAPI сервис
|
||||
- `worker` — Celery worker + beat (асинхронные задачи)
|
||||
- `nginx` — Reverse proxy
|
||||
|
||||
Backend API доступен:
|
||||
- Прямой: `http://localhost:8000/api/health`
|
||||
- Через Nginx: `http://localhost/api/health`
|
||||
|
||||
## 3a. Добавить видеоконференции (профиль media: LiveKit + Coturn)
|
||||
|
||||
Для включения видеоконференций с LiveKit SFU + Coturn TURN сервером:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml --profile media up -d
|
||||
```
|
||||
|
||||
Это добавит:
|
||||
- `livekit` — SFU сервер (слушает на `ws://localhost:7880` для signaling, UDP 52000-52100 для media)
|
||||
- `coturn` — TURN/STUN relay сервер
|
||||
|
||||
**Проверка LiveKit:**
|
||||
```bash
|
||||
# Проверить что LiveKit доступен
|
||||
curl http://localhost:7880/health
|
||||
|
||||
# Проверить что Coturn слушает
|
||||
nc -uz localhost 3478 # STUN
|
||||
```
|
||||
|
||||
**Ручная проверка видео:**
|
||||
Откройте два браузерных окна (или вкладки) на `http://localhost:5173` (frontend):
|
||||
1. Первое окно: зарегистрируйтесь и войдите
|
||||
2. Оба окна: отройте страницу лобби (комнаты)
|
||||
3. Оба окна: нажмите "Войти" в одну и ту же комнату
|
||||
4. Проверьте видео-потоки в обоих окнах (должны видеть друг друга)
|
||||
|
||||
**Структура портов:**
|
||||
- `7880/tcp` — LiveKit WebSocket signaling (через nginx `/livekit/`)
|
||||
- `7881/tcp` — LiveKit HTTPS (опционально)
|
||||
- `52000-52100/udp` — Media stream (RTP/RTCP)
|
||||
- `3478/tcp,udp` — Coturn STUN
|
||||
- `3479/tcp,udp` — Coturn альтернативный
|
||||
- `5349/tcp,udp` — Coturn TURNS (TLS)
|
||||
|
||||
## 4. Миграции БД и тестовые данные
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
uv run alembic upgrade head
|
||||
uv run python -m scripts.seed
|
||||
```
|
||||
|
||||
Миграции:
|
||||
- Создают все 14 таблиц (users, teams, email_verification_tokens, conferences, conference_invitees, guest_access, conference_sessions, conference_participants, session_audio_tracks, phrases, chat_messages, email_deliveries, instance_settings, livekit_webhook_events)
|
||||
- Включают расширение PostgreSQL `btree_gist` (установлено, но текущей схемой не используется)
|
||||
|
||||
Идемпотентный сид (`scripts.seed`):
|
||||
- Создаёт только 1 админ-пользователя (учётные данные из `.env`, `SEED_ADMIN_EMAIL`/`SEED_ADMIN_PASSWORD`)
|
||||
- Конференции создаются пользователями динамически — предустановленных данных не требуется
|
||||
|
||||
## 5. Запуск Frontend (Разработка)
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Frontend запущен на `http://localhost:5173` с включённым hot-reload.
|
||||
|
||||
**CORS:** Frontend подключается к backend через прокси Vite:
|
||||
- Запрос `/api/*` → перенаправляется на `http://localhost:8000/api/*`
|
||||
- WebSocket `/ws/*` → перенаправляется на `http://localhost:8000/ws/*`
|
||||
- Это настроено в `frontend/vite.config.ts` (режим разработки)
|
||||
|
||||
## 6. Проверка
|
||||
|
||||
Проверить, что всё запущено:
|
||||
|
||||
```bash
|
||||
# Backend здоров
|
||||
curl http://localhost:8000/api/health
|
||||
|
||||
# Миграции БД применены
|
||||
cd backend && uv run alembic current
|
||||
|
||||
# Frontend доступен
|
||||
curl http://localhost:5173
|
||||
|
||||
# Worker жив (если запущен)
|
||||
docker compose -f deploy/docker-compose.yml exec worker celery -A workers.celery_app inspect ping
|
||||
```
|
||||
|
||||
## 7. Тестирование и проверка качества
|
||||
|
||||
```bash
|
||||
# Backend — lint + форматирование
|
||||
cd backend
|
||||
uv run ruff check . && uv run ruff format --check .
|
||||
|
||||
# Backend — проверка типов
|
||||
uv run mypy .
|
||||
|
||||
# Backend — unit тесты
|
||||
uv run pytest -q
|
||||
|
||||
# Frontend — lint
|
||||
cd frontend
|
||||
npm run lint
|
||||
|
||||
# Frontend — проверка сборки
|
||||
npm run build
|
||||
|
||||
# Валидация docker-compose
|
||||
docker compose -f deploy/docker-compose.yml config -q
|
||||
```
|
||||
|
||||
## 8. Пресеты инсталлятора
|
||||
|
||||
VidConf поддерживает **5 пресетов инсталлятора**:
|
||||
|
||||
1. **MVP-ядро** (лобби, конференции, календарь, закреплённые, гости) — минимум функций
|
||||
2. **+чат** — текстовое общение в конференции (WebSocket + Redis pub/sub)
|
||||
3. **+AI min** (CPU) — транскрибация + суммаризация на уровне `min`
|
||||
4. **+AI средний** (CPU опционально GPU) — уровень `medium`
|
||||
5. **+AI макс** (GPU обязателен) — уровень `max` для высокой нагрузки
|
||||
|
||||
Для локальной разработки используйте пресет 1 или 3 (с инсталлятором `./install.sh --preset 3`).
|
||||
|
||||
**Детали:** [docs/deploy/install.md](install.md) и [docs/architecture/adr/004-ai-tier-matrix.md](../architecture/adr/004-ai-tier-matrix.md)
|
||||
|
||||
## 9. Решение проблем
|
||||
|
||||
**Backend не может подключиться к БД:**
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml logs postgres
|
||||
```
|
||||
|
||||
**Ошибка подключения Redis:**
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml logs redis
|
||||
```
|
||||
|
||||
**Сборка Frontend не удаётся:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install --force # Повтор установки зависимостей
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Миграции не выполняются:**
|
||||
```bash
|
||||
cd backend
|
||||
uv run alembic downgrade base
|
||||
uv run alembic upgrade head
|
||||
```
|
||||
|
||||
For more details, see [README.md](../../README.md).
|
||||
526
docs/deploy/env.md
Normal file
526
docs/deploy/env.md
Normal file
@@ -0,0 +1,526 @@
|
||||
# Переменные окружения (.env)
|
||||
|
||||
Полный справочник переменных окружения VidConf. Все секреты хранятся **только** в `.env` и никогда не коммитятся в git.
|
||||
|
||||
## Соглашение
|
||||
|
||||
Значения читаются из файла `.env` (или переменных окружения) при старте приложения. Dev-шаблон см. в `.env.example`.
|
||||
|
||||
---
|
||||
|
||||
## База данных
|
||||
|
||||
### DATABASE_URL
|
||||
**Тип:** `str` | **Default:** `postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf`
|
||||
|
||||
**Описание:** Connection string для асинхронного драйвера SQLAlchemy (asyncpg).
|
||||
|
||||
**Пример:**
|
||||
```bash
|
||||
DATABASE_URL=postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf
|
||||
```
|
||||
|
||||
**Проде:** Используйте отдельного пользователя с минимальными привилегиями (только SELECT/INSERT/UPDATE на нужные таблицы).
|
||||
|
||||
### POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB
|
||||
**Для Docker Compose:** переменные инициализации контейнера PostgreSQL.
|
||||
|
||||
```bash
|
||||
POSTGRES_USER=vidconf
|
||||
POSTGRES_PASSWORD=change-me-secure
|
||||
POSTGRES_DB=vidconf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redis (очередь Celery)
|
||||
|
||||
### REDIS_URL
|
||||
**Тип:** `str` | **Default:** `redis://localhost:6379/0`
|
||||
|
||||
**Описание:** URL Redis для брокера Celery (очередь задач) и кэша сеансов.
|
||||
|
||||
**Пример:**
|
||||
```bash
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
REDIS_URL=redis://:password@redis.example.com:6379/1 # с паролем, БД 1
|
||||
```
|
||||
|
||||
**Проде:** Используйте Redis с паролем и настройте мониторинг/резервные копии.
|
||||
|
||||
---
|
||||
|
||||
## Конфигурация приложения
|
||||
|
||||
### Бутстрап настроек инстанса (первый старт backend, `BOOTSTRAP_*`)
|
||||
|
||||
Три переменные определяют начальное состояние настроек модулей при первом
|
||||
запуске backend. Используются инсталлятором `install.sh` для синхронизации
|
||||
пресетов поставки (пресеты 1–5 → матрица `BOOTSTRAP_*`).
|
||||
|
||||
#### BOOTSTRAP_CHAT_ENABLED
|
||||
|
||||
**Тип:** `bool` (строка: `true` | `false`) | **Default:** `false`
|
||||
|
||||
**Описание:** Включить чат в конференциях при первом старте backend.
|
||||
|
||||
```bash
|
||||
BOOTSTRAP_CHAT_ENABLED=false # пресеты 1, 3
|
||||
BOOTSTRAP_CHAT_ENABLED=true # пресеты 2, 4, 5
|
||||
```
|
||||
|
||||
**Применение:** однократно при первом старте (lifespan backend), затем игнорируется
|
||||
(настройка хранится в БД, `instance_settings.chat.enabled`). Изменение переменной
|
||||
на живой инсталляции не действует — меняйте через админ-API `PUT /api/v1/admin/settings`.
|
||||
|
||||
---
|
||||
|
||||
#### BOOTSTRAP_TRANSCRIPTION_ENABLED
|
||||
|
||||
**Тип:** `bool` | **Default:** `false`
|
||||
|
||||
**Описание:** Включить транскрибацию и суммаризацию при первом старте backend.
|
||||
|
||||
```bash
|
||||
BOOTSTRAP_TRANSCRIPTION_ENABLED=false # пресеты 1, 2
|
||||
BOOTSTRAP_TRANSCRIPTION_ENABLED=true # пресеты 3, 4, 5
|
||||
```
|
||||
|
||||
**Применение:** однократно при первом старте (lifespan backend), затем игнорируется.
|
||||
Выключение транскрибации = AI-уровень не применяется (см. ниже). На живой
|
||||
инсталляции меняйте через `PUT /api/v1/admin/settings?transcription_enabled=...`.
|
||||
|
||||
**Guard:** если транскрибация включена в настройках (`transcription_enabled=true`),
|
||||
но ни один Celery-воркер `transcriber` не обслуживает очередь, в админке
|
||||
отображается предупреждение (поле `transcription_queue_served` в ответе
|
||||
`GET /api/v1/admin/settings`).
|
||||
|
||||
---
|
||||
|
||||
#### BOOTSTRAP_AI_LEVEL
|
||||
|
||||
**Тип:** `str` | **Default:** `min` | **Допустимые:** `min`, `medium`, `max`
|
||||
|
||||
**Описание:** Уровень качества AI-обработки при первом старте backend
|
||||
(требует `BOOTSTRAP_TRANSCRIPTION_ENABLED=true`).
|
||||
|
||||
```bash
|
||||
BOOTSTRAP_AI_LEVEL=min # пресеты 1–3
|
||||
BOOTSTRAP_AI_LEVEL=medium # пресет 4
|
||||
BOOTSTRAP_AI_LEVEL=max # пресет 5
|
||||
```
|
||||
|
||||
**Матрица инсталлятора (пресет → `BOOTSTRAP_*`):**
|
||||
|
||||
| Пресет | `BOOTSTRAP_CHAT_ENABLED` | `BOOTSTRAP_TRANSCRIPTION_ENABLED` | `BOOTSTRAP_AI_LEVEL` |
|
||||
|--------|:---:|:---:|---|
|
||||
| 1 (MVP) | `false` | `false` | `min` |
|
||||
| 2 (+чат) | `true` | `false` | `min` |
|
||||
| 3 (+AI мин) | `true` | `true` | `min` |
|
||||
| 4 (+AI средний) | `true` | `true` | `medium` |
|
||||
| 5 (+AI макс) | `true` | `true` | `max` |
|
||||
|
||||
**Применение:** однократно при первом старте, затем игнорируется
|
||||
(настройка в `instance_settings.ai_level`). На живой инсталляции меняйте
|
||||
через `PUT /api/v1/admin/settings?ai_level=...` (проверяется доступность уровня,
|
||||
недоступный уровень → 400).
|
||||
|
||||
---
|
||||
|
||||
### PLUGINS_CONFIG_PATH
|
||||
**Тип:** `str` | **Default:** `config/plugins.yaml`
|
||||
|
||||
**Описание:** Путь к конфигурационному файлу плагинов (от корня проекта или абсолютный).
|
||||
|
||||
```bash
|
||||
PLUGINS_CONFIG_PATH=config/plugins.yaml
|
||||
```
|
||||
|
||||
Содержит профили AI: transcriber (faster-whisper small/medium/large-v3), summarizer (Qwen3.5 4B/9B/35B-A3B), chat (отключён/включён).
|
||||
|
||||
---
|
||||
|
||||
## Пользователь seed (начальный администратор)
|
||||
|
||||
### SEED_ADMIN_EMAIL
|
||||
**Тип:** `str` | **Default:** `admin@vidconf.example`
|
||||
|
||||
**Описание:** Email администратора, создаваемого при инициализации БД (`uv run python -m scripts.seed`, см. [backend/README.md](../../backend/README.md)).
|
||||
|
||||
```bash
|
||||
SEED_ADMIN_EMAIL=admin@example.com
|
||||
```
|
||||
|
||||
### SEED_ADMIN_PASSWORD
|
||||
**Тип:** `str` | **Default:** `change-me`
|
||||
|
||||
**Описание:** Пароль администратора. **Измените на боевом инстансе сразу после развёртывания!**
|
||||
|
||||
```bash
|
||||
SEED_ADMIN_PASSWORD=SuperSecurePassword123!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Аутентификация и сессии (JWT)
|
||||
|
||||
### JWT_SECRET
|
||||
**Тип:** `str` | **Default:** `dev-only-insecure-secret-change-me`
|
||||
|
||||
**Описание:** Секретный ключ для подписи JWT токенов (HS256).
|
||||
|
||||
**Требование:** Минимум 32 символа случайных данных для проде.
|
||||
|
||||
```bash
|
||||
# Генерировать:
|
||||
# python -c "import secrets; print(secrets.token_urlsafe(32))"
|
||||
JWT_SECRET=your-long-random-secret-generated-above
|
||||
```
|
||||
|
||||
### ACCESS_TOKEN_TTL_MINUTES
|
||||
**Тип:** `int` | **Default:** `15`
|
||||
|
||||
**Описание:** Срок действия access-токена в минутах.
|
||||
|
||||
```bash
|
||||
ACCESS_TOKEN_TTL_MINUTES=15
|
||||
```
|
||||
|
||||
### REFRESH_TOKEN_TTL_DAYS
|
||||
**Тип:** `int` | **Default:** `14`
|
||||
|
||||
**Описание:** Срок действия refresh-токена в днях. При истечении пользователь должен перелогиниться.
|
||||
|
||||
```bash
|
||||
REFRESH_TOKEN_TTL_DAYS=14
|
||||
```
|
||||
|
||||
### EMAIL_VERIFICATION_TTL_HOURS
|
||||
**Тип:** `int` | **Default:** `24`
|
||||
|
||||
**Описание:** Срок действия email-верификационного токена в часах (при регистрации).
|
||||
|
||||
```bash
|
||||
EMAIL_VERIFICATION_TTL_HOURS=24
|
||||
```
|
||||
|
||||
### AUTH_COOKIE_SECURE
|
||||
**Тип:** `bool` | **Default:** `true`
|
||||
|
||||
**Описание:** Флаг `Secure` для cookies, содержащих refresh-токены.
|
||||
|
||||
- **true** (проде) — cookies отправляются только по HTTPS
|
||||
- **false** (dev на localhost) — cookies отправляются и по HTTP (необходимо для Safari на localhost без HTTPS)
|
||||
|
||||
```bash
|
||||
AUTH_COOKIE_SECURE=false # только для dev
|
||||
AUTH_COOKIE_SECURE=true # проде обязательно
|
||||
```
|
||||
|
||||
### FRONTEND_URL
|
||||
**Тип:** `str` | **Default:** `http://localhost:5173`
|
||||
|
||||
**Описание:** URL фронтенда. Используется для формирования ссылок в email'ах подтверждения (обычно не требуется, фронтенд за nginx'ом).
|
||||
|
||||
```bash
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
FRONTEND_URL=https://vidconf.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LiveKit SFU
|
||||
|
||||
### LIVEKIT_URL
|
||||
**Тип:** `str` | **Default:** `ws://localhost:7880`
|
||||
|
||||
**Описание:** Внутренний server-to-server URL LiveKit (для Celery задач, вызовы RoomService). Не проксируется через Nginx.
|
||||
|
||||
```bash
|
||||
LIVEKIT_URL=ws://localhost:7880
|
||||
LIVEKIT_URL=http://livekit:7880 # docker-сеть
|
||||
```
|
||||
|
||||
### LIVEKIT_PUBLIC_URL
|
||||
**Тип:** `str` | **Default:** `ws://localhost:7880`
|
||||
|
||||
**Описание:** Публичный URL LiveKit (браузер клиента). Обычно за nginx'ом с TLS.
|
||||
|
||||
```bash
|
||||
LIVEKIT_PUBLIC_URL=ws://localhost:7880 # dev
|
||||
LIVEKIT_PUBLIC_URL=wss://livekit.example.com # проде (WSS)
|
||||
```
|
||||
|
||||
### LIVEKIT_API_KEY
|
||||
**Тип:** `str` | **Default:** `devkey`
|
||||
|
||||
**Описание:** API ключ LiveKit (для генерации токенов комнат).
|
||||
|
||||
```bash
|
||||
LIVEKIT_API_KEY=your-api-key
|
||||
```
|
||||
|
||||
### LIVEKIT_API_SECRET
|
||||
**Тип:** `str` | **Default:** `change-me-livekit-secret`
|
||||
|
||||
**Описание:** API секрет LiveKit (для подписи токенов).
|
||||
|
||||
```bash
|
||||
LIVEKIT_API_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
Оба найдите в конфигурации LiveKit сервера (`livekit.conf`).
|
||||
|
||||
---
|
||||
|
||||
## TURN (Coturn)
|
||||
|
||||
### TURN_REALM
|
||||
**Тип:** `str` | **Default:** `vidconf.local`
|
||||
|
||||
**Описание:** Realm для TURN сервера (Coturn). Испльзуется в credentials для WebRTC NAT traversal.
|
||||
|
||||
```bash
|
||||
TURN_REALM=vidconf.example.com
|
||||
```
|
||||
|
||||
### TURN_STATIC_AUTH_SECRET
|
||||
**Тип:** `str` | **Default:** `change-me-turn-secret`
|
||||
|
||||
**Описание:** Секрет для TURN сервера (для временных credentials).
|
||||
|
||||
```bash
|
||||
TURN_STATIC_AUTH_SECRET=your-secret-auth-key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Аватары
|
||||
|
||||
### MEDIA_ROOT
|
||||
**Тип:** `str` | **Default:** `/app/media`
|
||||
|
||||
**Описание:** Абсолютный путь к каталогу хранения загруженных аватаров пользователей.
|
||||
|
||||
```bash
|
||||
MEDIA_ROOT=/app/media # Docker Compose (по умолчанию)
|
||||
MEDIA_ROOT=/var/lib/vidconf/media # альтернативный путь на сервере
|
||||
```
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
MEDIA_ROOT/
|
||||
└── avatars/
|
||||
├── 550e8400-e29b-41d4-a716-446655440000.jpg
|
||||
├── 6ba7b811-9dad-11d1-80b4-00c04fd430c8.png
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Docker Compose:** том `media` монтируется в backend и nginx. Nginx раздаёт файлы напрямую по `location /media/` в обход backend'а (для performance).
|
||||
|
||||
---
|
||||
|
||||
## Транскрибация
|
||||
|
||||
### RECORDINGS_DIR
|
||||
**Тип:** `str` | **Default:** `/recordings`
|
||||
|
||||
**Описание:** Абсолютный путь к тому с записями (LiveKit Egress пишет .ogg треки сюда, Celery воркер их читает).
|
||||
|
||||
```bash
|
||||
RECORDINGS_DIR=/recordings
|
||||
RECORDINGS_DIR=/mnt/recordings # альтернативный путь
|
||||
```
|
||||
|
||||
**Docker Compose:** том `recordings` монтируется в обе сервиса (egress, transcriber). Менять путь внутри контейнера обычно не нужно (он совпадает с точкой монтирования).
|
||||
|
||||
---
|
||||
|
||||
## Email
|
||||
|
||||
Все SMTP-реквизиты — **только в `.env`, никогда не попадают в БД или API**.
|
||||
|
||||
### EMAIL_BACKEND
|
||||
**Тип:** `str` | **Default:** `console`
|
||||
|
||||
**Допустимые значения:**
|
||||
- `console` — dev: письма только логируются в stdout, ссылки берутся из логов
|
||||
- `smtp` — реальная отправка через aiosmtplib
|
||||
|
||||
```bash
|
||||
EMAIL_BACKEND=console # dev
|
||||
EMAIL_BACKEND=smtp # проде
|
||||
```
|
||||
|
||||
### SMTP_HOST
|
||||
**Тип:** `str` | **Default:** `localhost`
|
||||
|
||||
**Описание:** Хост SMTP сервера (для `EMAIL_BACKEND=smtp`).
|
||||
|
||||
```bash
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_HOST=mail.example.com
|
||||
```
|
||||
|
||||
### SMTP_PORT
|
||||
**Тип:** `int` | **Default:** `587`
|
||||
|
||||
**Описание:** Порт SMTP (обычно 587 для TLS, 465 для SSL, 25 без шифрования).
|
||||
|
||||
```bash
|
||||
SMTP_PORT=587 # TLS (STARTTLS)
|
||||
SMTP_PORT=465 # SSL
|
||||
SMTP_PORT=25 # plain
|
||||
```
|
||||
|
||||
### SMTP_USERNAME
|
||||
**Тип:** `str | None` | **Default:** `None`
|
||||
|
||||
**Описание:** Логин SMTP (если требуется аутентификация).
|
||||
|
||||
```bash
|
||||
SMTP_USERNAME=noreply@vidconf.example.com
|
||||
SMTP_USERNAME=
|
||||
```
|
||||
|
||||
### SMTP_PASSWORD
|
||||
**Тип:** `str | None` | **Default:** `None`
|
||||
|
||||
**Описание:** Пароль SMTP.
|
||||
|
||||
```bash
|
||||
SMTP_PASSWORD=your-app-password
|
||||
SMTP_PASSWORD=
|
||||
```
|
||||
|
||||
### SMTP_START_TLS
|
||||
**Тип:** `bool` | **Default:** `true`
|
||||
|
||||
**Описание:** Использовать STARTTLS (команда TLS после SMTP HELLO). Обычно для порта 587.
|
||||
|
||||
```bash
|
||||
SMTP_START_TLS=true # порт 587
|
||||
SMTP_START_TLS=false # если уже SSL или plain
|
||||
```
|
||||
|
||||
### SMTP_USE_TLS
|
||||
**Тип:** `bool` | **Default:** `false`
|
||||
|
||||
**Описание:** Использовать implicit TLS (сразу шифрованное соединение). Обычно для порта 465.
|
||||
|
||||
```bash
|
||||
SMTP_USE_TLS=false # для STARTTLS (587)
|
||||
SMTP_USE_TLS=true # для implicit TLS (465)
|
||||
```
|
||||
|
||||
### SMTP_FROM
|
||||
**Тип:** `str` | **Default:** `VidConf <no-reply@vidconf.example>`
|
||||
|
||||
**Описание:** From адрес в письмах (имя + email).
|
||||
|
||||
```bash
|
||||
SMTP_FROM=VidConf <no-reply@vidconf.example>
|
||||
SMTP_FROM=noreply@example.com
|
||||
```
|
||||
|
||||
### SMTP_TIMEOUT_S
|
||||
**Тип:** `int` | **Default:** `30`
|
||||
|
||||
**Описание:** Таймаут SMTP операций в секундах.
|
||||
|
||||
```bash
|
||||
SMTP_TIMEOUT_S=30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примеры конфигурации
|
||||
|
||||
### Dev (console backend)
|
||||
```bash
|
||||
# Database & Redis
|
||||
DATABASE_URL=postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
|
||||
# App
|
||||
PLUGINS_CONFIG_PATH=config/plugins.yaml
|
||||
JWT_SECRET=dev-only-secret-change-me
|
||||
FRONTEND_URL=http://localhost:5173
|
||||
|
||||
# LiveKit
|
||||
LIVEKIT_URL=ws://localhost:7880
|
||||
LIVEKIT_PUBLIC_URL=ws://localhost:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=...
|
||||
|
||||
# Seed
|
||||
SEED_ADMIN_EMAIL=admin@vidconf.example
|
||||
SEED_ADMIN_PASSWORD=change-me
|
||||
|
||||
# Email (только логирование)
|
||||
EMAIL_BACKEND=console
|
||||
|
||||
# Media (аватары)
|
||||
MEDIA_ROOT=/app/media
|
||||
```
|
||||
|
||||
### Проде (SMTP, TLS)
|
||||
```bash
|
||||
# Database & Redis
|
||||
DATABASE_URL=postgresql+asyncpg://vidconf:secure-pass@db.example.com:5432/vidconf
|
||||
REDIS_URL=redis://:redis-password@redis.example.com:6379/0
|
||||
|
||||
# App
|
||||
PLUGINS_CONFIG_PATH=config/plugins.yaml
|
||||
JWT_SECRET=<generated-long-random-key>
|
||||
ACCESS_TOKEN_TTL_MINUTES=15
|
||||
REFRESH_TOKEN_TTL_DAYS=14
|
||||
AUTH_COOKIE_SECURE=true
|
||||
FRONTEND_URL=https://vidconf.example.com
|
||||
|
||||
# LiveKit
|
||||
LIVEKIT_URL=http://livekit:7880
|
||||
LIVEKIT_PUBLIC_URL=wss://livekit.example.com
|
||||
LIVEKIT_API_KEY=your-key
|
||||
LIVEKIT_API_SECRET=your-secret
|
||||
|
||||
# Seed
|
||||
SEED_ADMIN_EMAIL=admin@vidconf.example
|
||||
SEED_ADMIN_PASSWORD=<strong-password>
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND=smtp
|
||||
SMTP_HOST=smtp.sendgrid.net
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=apikey
|
||||
SMTP_PASSWORD=SG.xxxxx...
|
||||
SMTP_START_TLS=true
|
||||
SMTP_USE_TLS=false
|
||||
SMTP_FROM=VidConf <noreply@vidconf.example>
|
||||
SMTP_TIMEOUT_S=30
|
||||
|
||||
# Media (аватары)
|
||||
MEDIA_ROOT=/var/lib/vidconf/media
|
||||
|
||||
# Recordings
|
||||
RECORDINGS_DIR=/mnt/recordings
|
||||
|
||||
# Transcription
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Безопасность
|
||||
|
||||
1. **Никогда не коммитьте `.env` в git** — используйте `.gitignore`
|
||||
2. **Регулярно ротируйте JWT_SECRET** (потребует переlogin'ования пользователей)
|
||||
3. **Используйте окружение для production credentials** (никогда не hardcod'ьте)
|
||||
4. **Проверьте SMTP_PASSWORD в логах** — их быть не должно (логирование маскирует пароли)
|
||||
5. **Для SMTP используйте app passwords** (не основной пароль аккаунта)
|
||||
|
||||
---
|
||||
|
||||
## Ссылки
|
||||
|
||||
- [.env.example](../../.env.example) — шаблон переменных для dev
|
||||
- [backend/core/config.py](../../backend/core/config.py) — парсинг Settings в коде
|
||||
64
docs/deploy/hardware-profiles.md
Normal file
64
docs/deploy/hardware-profiles.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Профили оборудования и пресеты инсталлятора
|
||||
|
||||
VidConf использует **5 пресетов инсталлятора** (на основе 2 измерений: чат вкл/выкл и уровень AI мин/средний/макс) и **3 уровня AI качества** для настройки под разные сценарии развёртывания.
|
||||
|
||||
Вся информация здесь основана на источниках истины:
|
||||
- **[docs/deploy/install.md](install.md)** — описание 5 пресетов, использование `install.sh`, автодетект железа
|
||||
- **[docs/architecture/adr/004-ai-tier-matrix.md](../architecture/adr/004-ai-tier-matrix.md)** — матрица уровней AI (модели, кванты, требования железа, параметры генерации)
|
||||
|
||||
## Пресеты инсталлятора (5 вариантов)
|
||||
|
||||
| # | Состав | Композе-профили | Модели | Сложность |
|
||||
|---|--------|---|--------|-----------|
|
||||
| 1 | MVP-ядро (лобби, конференции, календарь, закреплённые, гости) | `media` | — | Минимум |
|
||||
| 2 | 1 + чат (WebSocket, Redis pub/sub) | `media` | — | Низкая |
|
||||
| 3 | 2 + AI «мин» (CPU) | `media,transcribe,llm` | faster-whisper `small` + Qwen3.5-4B | Средняя |
|
||||
| 4 | 2 + AI «средний» (CPU опционально GPU) | `media,transcribe,llm` | faster-whisper `medium` + Qwen3.5-9B | Средняя |
|
||||
| 5 | 2 + AI «макс» (GPU обязателен) | `media,transcribe-gpu,llm-gpu` | faster-whisper `large-v3` + Qwen3.5-35B-A3B | Высокая |
|
||||
|
||||
**Использование:** `./install.sh --preset N` или интерактивный опросник для автоматического выбора пресета на основе детекта железа.
|
||||
|
||||
## Уровни AI (матрица уровней)
|
||||
|
||||
Три уровня качества обработки, применяемые к пресетам 3–5:
|
||||
|
||||
| Уровень | Транскрибация | Суммаризация | Требования (мин) | GPU |
|
||||
|---------|---|---|---|---|
|
||||
| **min** | faster-whisper `small` | Qwen3.5-4B (GGUF Q4_K_M, ~2,8 ГБ) | 8 vCPU, 16 ГБ RAM | нет |
|
||||
| **medium** | faster-whisper `medium` | Qwen3.5-9B (GGUF Q4_K_M, ~6,2 ГБ) | 12–16 vCPU, 32 ГБ RAM | опционально ≥8 ГБ |
|
||||
| **max** | faster-whisper `large-v3` | Qwen3.5-35B-A3B (GGUF Q4_K_M, ~20–22 ГБ) | 16+ vCPU, 64 ГБ RAM | **обязательно ≥16 ГБ** |
|
||||
|
||||
Детальная матрица с параметрами модели (режимы compute, max_tokens per-tier, CTX) — [ADR-004](../architecture/adr/004-ai-tier-matrix.md).
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Установка с автодетектом
|
||||
|
||||
```bash
|
||||
# Интерактивный опросник (рекомендуемый вариант)
|
||||
./install.sh
|
||||
|
||||
# Или неинтерактивно: пресет 3 (AI min)
|
||||
./install.sh --preset 3
|
||||
|
||||
# Или без подтверждений (CI/scripts, warning: пресет 5 на non-GPU)
|
||||
./install.sh --preset 3 --yes
|
||||
```
|
||||
|
||||
`install.sh` автоматически:
|
||||
1. Детектирует CPU/RAM/GPU (nvidia-smi, sysctl, free)
|
||||
2. Рекомендует пресет
|
||||
3. Генерирует/обновляет `.env` (секреты сохраняются)
|
||||
4. Запускает `docker compose` с нужными профилями
|
||||
5. Выполняет миграции и seed
|
||||
|
||||
Полные детали: [docs/deploy/install.md](install.md).
|
||||
|
||||
## Ссылки
|
||||
|
||||
- **[Инсталлятор](install.md)** — подробное описание `install.sh` и пресетов
|
||||
- **[ADR-004: Матрица уровней AI](../architecture/adr/004-ai-tier-matrix.md)** — модели, требования, параметры
|
||||
- **[LLM Setup](llm-setup.md)** — ручная установка/скачивание моделей
|
||||
- **[Deploy: Мониторинг](monitoring.md)** — Prometheus/Grafana, алерты
|
||||
- **[Deploy: Масштабирование](scaling.md)** — горизонтальное масштабирование
|
||||
- **[Deploy: Ёмкость](capacity.md)** — калькулятор нагрузки и IOPS
|
||||
134
docs/deploy/install.md
Normal file
134
docs/deploy/install.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Инсталлятор (`install.sh`, релиз v0.0.1)
|
||||
|
||||
Один скрипт в корне репозитория — автодетект железа, опросник из 5
|
||||
пресетов поставки, идемпотентная запись `.env`, сборка образов, миграции и
|
||||
seed, подъём стека. По завершении `http://localhost/` отдаёт рабочий
|
||||
фронтенд-SPA (React + Vite; статика вкомпилирована в образ nginx,
|
||||
`frontend/Dockerfile`) — во ВСЕХ пресетах. Источник истины по
|
||||
моделям/квантам/требованиям железа — ADR-004
|
||||
(`docs/architecture/adr/004-ai-tier-matrix.md`); по пресетам поставки —
|
||||
сам `install.sh` (см. таблицу ниже).
|
||||
|
||||
## Пресеты
|
||||
|
||||
| # | Состав | Профили compose | Модели |
|
||||
|---|---|---|---|
|
||||
| 1 | MVP-ядро (лобби, конференции, календарь, закреплённые, гости) | `media` | — |
|
||||
| 2 | 1 + чат | `media` | — |
|
||||
| 3 | 2 + AI «мин» (CPU) | `media,transcribe,llm` | faster-whisper `small` + Qwen3.5-4B |
|
||||
| 4 | 2 + AI «средний» (CPU) | `media,transcribe,llm` | faster-whisper `medium` + Qwen3.5-9B |
|
||||
| 5 | 2 + AI «макс» (GPU обязателен) | `media,transcribe-gpu,llm-gpu` | faster-whisper `large-v3` + Qwen3.5-35B-A3B |
|
||||
|
||||
Чат (2) и уровень AI (3/4/5) — переключатели плагинов в админке
|
||||
(`instance_settings`, БД); `install.sh` поднимает НЕОБХОДИМЫЕ для
|
||||
выбранного пресета контейнеры/модели И синхронизирует настройки инстанса
|
||||
в соответствии с матрицей пресета (см. раздел «Синхронизация настроек
|
||||
модулей»). Уровень `medium` в текущей матрице (`backend/services/ai_tiers.py`)
|
||||
CPU-only — отдельного GPU-варианта профилей для пресета 4 нет (GPU для
|
||||
`medium` — ручная настройка администратора вне детекта).
|
||||
|
||||
Запись конференций НЕ входит в пресеты (появится в v0.1.0) — вопрос
|
||||
про неё инсталлятор не задаёт.
|
||||
|
||||
## Синхронизация настроек модулей (бутстрап)
|
||||
|
||||
При **первом старте** backend (lifespan) применяет пресет настроек через
|
||||
три env-переменные `BOOTSTRAP_*`:
|
||||
|
||||
- `BOOTSTRAP_CHAT_ENABLED` (`true` | `false`) — включить чат в конференциях
|
||||
- `BOOTSTRAP_TRANSCRIPTION_ENABLED` (`true` | `false`) — включить транскрибацию и суммаризацию
|
||||
- `BOOTSTRAP_AI_LEVEL` (`min` | `medium` | `max`) — уровень AI (игнорируется, если транскрибация выключена)
|
||||
|
||||
**Матрица пресет → настройки модулей:**
|
||||
|
||||
| Пресет | Чат | AI | Уровень | `BOOTSTRAP_CHAT_ENABLED` | `BOOTSTRAP_TRANSCRIPTION_ENABLED` | `BOOTSTRAP_AI_LEVEL` |
|
||||
|--------|-----|-----|---------|:---:|:---:|---|
|
||||
| 1 (MVP-ядро) | — | — | — | `false` | `false` | `min` |
|
||||
| 2 (+чат) | ✓ | — | — | `true` | `false` | `min` |
|
||||
| 3 (+AI мин) | ✓ | ✓ | мин | `true` | `true` | `min` |
|
||||
| 4 (+AI средний) | ✓ | ✓ | средний | `true` | `true` | `medium` |
|
||||
| 5 (+AI макс) | ✓ | ✓ | макс | `true` | `true` | `max` |
|
||||
|
||||
Бутстрап **идемпотентен** при первом старте: дефолты из `config/plugins.yaml`
|
||||
(всё `enabled: true`) переопределяются переменными `BOOTSTRAP_*` только если
|
||||
БД ещё не содержит ключи настроек инстанса (проверка `INSERT ... ON CONFLICT DO NOTHING`).
|
||||
Повторное включение контейнера backend не перетирает существующие настройки.
|
||||
|
||||
**При повторном запуске** инсталлятора на живой инсталляции:
|
||||
|
||||
```bash
|
||||
./install.sh --preset 3 # спросит: обновить настройки? [Y/n]
|
||||
./install.sh --preset 3 --yes # без подтверждения, применит пресет 3
|
||||
```
|
||||
|
||||
Поведение:
|
||||
- **Дефолт (Enter):** применяет матрицу выбранного пресета к настройкам БД
|
||||
(defs: `docker compose exec -T backend uv run python -m scripts.apply_preset_settings --force`)
|
||||
- **Отказ (`n`):** сохраняет ручные правки админа; переменные `BOOTSTRAP_*` в `.env`
|
||||
обновляются, но скрипт применения настроек НЕ запускается
|
||||
- **Флаг `--yes`:** автоматически применяет пресет без вопроса (для CI/CD)
|
||||
|
||||
Скрипт обновления: upsert четырёх ключей в `instance_settings` БД:
|
||||
`chat` (`enabled`), `transcriber` (`enabled`), `summarizer` (`enabled`), `ai_level`.
|
||||
|
||||
## Использование
|
||||
|
||||
```bash
|
||||
./install.sh # интерактивный опросник + рекомендация по железу
|
||||
./install.sh --preset 3 # неинтерактивно
|
||||
./install.sh --preset 3 --yes # без подтверждений (скрипты/CI, пресет 5 без GPU)
|
||||
```
|
||||
|
||||
Повторный запуск с другим `--preset` — апгрейд/даунгрейд на месте: модели
|
||||
уровня докачиваются (старые с диска не удаляются — занимают место, но не
|
||||
мешают), `COMPOSE_PROFILES`/`WHISPER_MODEL`/`LLM_MODEL_*` в `.env`
|
||||
обновляются точечно, секреты (JWT/БД/TURN/LiveKit/Grafana) и любые
|
||||
пользовательские правки `.env` — сохраняются (генерация только при полном
|
||||
отсутствии значения, см. `ensure_secret` в `install.sh`).
|
||||
|
||||
## Что делает скрипт
|
||||
|
||||
1. Автодетект `nproc`/`free -m` (или `sysctl` на macOS)/`nvidia-smi` →
|
||||
пишет `HW_CPUS`/`HW_RAM_MB`/`HW_GPU_NAME`/`HW_VRAM_MB` в `.env` — их
|
||||
читает `backend/services/ai_levels.py` (детект доступности уровней AI в
|
||||
админке, причины недоступности).
|
||||
2. Точечно обновляет `.env` (создаёт из `.env.example` при первом запуске,
|
||||
на первом запуске сразу генерирует РЕАЛЬНЫЕ секреты вместо
|
||||
плейсхолдеров `change-me...` из шаблона в git).
|
||||
3. Собирает флаги `--profile` из `COMPOSE_PROFILES` в `.env` (эта версия
|
||||
docker compose НЕ подхватывает `COMPOSE_PROFILES` автоматически при `up`,
|
||||
поэтому профили media/transcribe/llm передаются командам явно) и собирает
|
||||
образы: `docker compose pull --ignore-buildable` (best-effort) →
|
||||
`docker compose build` (backend, worker и nginx — образ nginx multi-stage
|
||||
собирает фронтенд-SPA и вкомпилирует статику, `frontend/Dockerfile`).
|
||||
4. Поднимает `postgres`/`redis` (`up -d --wait`) и применяет **до старта
|
||||
backend** миграции и seed одноразовыми контейнерами:
|
||||
`docker compose run --rm backend uv run alembic upgrade head` +
|
||||
`... python -m scripts.seed`. Порядок критичен: `backend.lifespan`
|
||||
бутстрапит `instance_settings` при каждом старте приложения, поэтому на
|
||||
чистой БД таблицы обязаны существовать до первого запуска backend — иначе
|
||||
healthcheck (`--wait`) никогда не проходит. Seed идемпотентен (админ;
|
||||
справочник `teams` и ключи `instance_settings`, включая
|
||||
`registration_team_choice`/`registration_email_domain`, бутстрапятся
|
||||
backend'ом при старте, `INSERT ... ON CONFLICT DO NOTHING` — существующие
|
||||
настройки инстанса не перетираются).
|
||||
5. Поднимает остальной стек: `docker compose <--profile ...> up -d --wait`
|
||||
(backend, worker, nginx с фронтом + сервисы активных профилей). Команда
|
||||
идемпотентна и обёрнута в ретрай (до 3 попыток): первый старт backend/worker
|
||||
включает `uv run` (синхронизация окружения + компиляция байткода), и на
|
||||
слабой/загруженной машине healthcheck может не успеть за отведённые
|
||||
retries — повтор лишь дожидается уже стартующих контейнеров.
|
||||
6. Печатает сводку: URL фронтенда/бэкенда, учётные данные администратора,
|
||||
команда для `--profile monitoring`.
|
||||
|
||||
## Проверка
|
||||
|
||||
- Чистая установка (пресеты 1 и 3) на пустом `.env`/чистых volume.
|
||||
- Апгрейд 1 → 3 (повторный запуск с другим `--preset`, секреты сохранены).
|
||||
- Рекомендация детекта соответствует реальному железу текущей машины.
|
||||
- `bash -n install.sh`, `docker compose -f deploy/docker-compose.yml config -q`
|
||||
для всех сочетаний профилей — минимальная валидация без реального подъёма.
|
||||
|
||||
Полную установку на чистой машине/VM гоняют вручную (использование сети
|
||||
для скачивания GGUF-моделей ~2,6–20,5 ГБ, GPU-хост для пресета 5) — вне
|
||||
рамок автоматической проверки.
|
||||
121
docs/deploy/llm-setup.md
Normal file
121
docs/deploy/llm-setup.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# Настройка локального LLM-сервера (Qwen3.5, профили compose `llm`/`llm-gpu`)
|
||||
|
||||
Плагин суммаризации `qwen_local` (`backend/core/plugins/qwen_local.py`)
|
||||
обращается к OpenAI-совместимому серверу `llama.cpp`. Модель и параметры —
|
||||
единый источник истины ADR-004 (`docs/architecture/adr/004-ai-tier-matrix.md`)
|
||||
и константная матрица `backend/services/ai_tiers.py`. В обычной установке всё
|
||||
описанное ниже делает `install.sh` (см. `docs/deploy/dev-setup.md`) — этот
|
||||
раздел актуален для ручного/точечного запуска профиля без инсталлятора.
|
||||
|
||||
## 1. Компоненты
|
||||
|
||||
| Сервис | Образ | Профиль | Назначение |
|
||||
|---|---|---|---|
|
||||
| `llm-models-init` | `busybox` | `llm`/`llm-gpu` | фиксирует владельца тома `llm-models` (обходит дефект прав доступа) |
|
||||
| `llm-model-init` | `curlimages/curl` | `llm`/`llm-gpu` | однократное скачивание GGUF-модели и tokenizer.json уровня AI |
|
||||
| `llm` | `ghcr.io/ggml-org/llama.cpp:server-b<build>` | `llm` | CPU-инференс (`/v1/chat/completions`), уровни `min`/`medium` |
|
||||
| `llm-gpu` | `ghcr.io/ggml-org/llama.cpp:server-cuda-b<build>` | `llm-gpu` | GPU-инференс, уровень `max` (ADR-004: GPU обязателен) |
|
||||
|
||||
Файлы:
|
||||
- `deploy/llm/download-model.sh` — генерализованный скрипт скачивания
|
||||
(параметризован `LLM_MODEL_FILE`/`LLM_MODEL_URL`/`LLM_MODEL_MIN_SIZE`/
|
||||
`LLM_TOKENIZER_FILE`/`LLM_TOKENIZER_URL`; их пишет `install.sh` по
|
||||
выбранному пресету — см. `install.sh --help`).
|
||||
- `deploy/docker-compose.yml` — сервисы `llm-models-init`/`llm-model-init`/
|
||||
`llm`/`llm-gpu`, volume `llm-models`, монтирование
|
||||
`llm-models:/models/qwen:ro` в сервис `worker` (там же считаются токены
|
||||
чанкером — `QwenTokenCounter`).
|
||||
|
||||
## 2. Модели по уровням AI (ADR-004)
|
||||
|
||||
| Уровень | Модель | Квант | Файл (`LLM_MODEL_FILE`) | Источник GGUF (`LLM_MODEL_URL`) |
|
||||
|---|---|---|---|---|
|
||||
| `min` (пресет 3) | Qwen3.5-4B-Instruct | Q4_K_M ≈ 2,6 ГиБ | `qwen3.5-4b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-4B-GGUF` |
|
||||
| `medium` (пресет 4) | Qwen3.5-9B-Instruct | Q4_K_M ≈ 5,3 ГиБ | `qwen3.5-9b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-9B-GGUF` |
|
||||
| `max` (пресет 5) | Qwen3.5-35B-A3B-Instruct (MoE) | Q4_K_M ≈ 20,5 ГиБ | `qwen3.5-35b-a3b-instruct-q4_k_m.gguf` | `unsloth/Qwen3.5-35B-A3B-GGUF` |
|
||||
|
||||
`tokenizer.json` (переименован под `LLM_TOKENIZER_FILE` — нужен
|
||||
`QwenTokenCounter`, `backend/core/summarization/tokens.py`) берётся из
|
||||
официальных репозиториев `Qwen/Qwen3.5-<размер>` (публичные, без gate;
|
||||
GGUF-репозитории `Qwen/…-GGUF` — gated, поэтому источник GGUF — публичное
|
||||
зеркало `unsloth/…-GGUF`, файлы идентичны по содержимому квантизации).
|
||||
|
||||
Имена файлов на диске (`LLM_MODEL_FILE`/`LLM_TOKENIZER_FILE`) — КОНТРАКТ с
|
||||
детектом доступности уровня AI (`backend/services/ai_levels.py` через
|
||||
`backend/services/ai_tiers.py::TIERS[level].model_files`): именно эти пути
|
||||
проверяются на «модель скачана» в админке.
|
||||
|
||||
Скачивание идемпотентно (проверка по наличию и минимальному размеру файла,
|
||||
`LLM_MODEL_MIN_SIZE`) — повторный запуск на уже заполненном томе ничего не
|
||||
перекачивает. Ручной запуск (прогреть volume заранее):
|
||||
|
||||
```bash
|
||||
LLM_MODEL_FILE=qwen3.5-4b-instruct-q4_k_m.gguf \
|
||||
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/resolve/main/Qwen3.5-4B-Q4_K_M.gguf \
|
||||
LLM_TOKENIZER_FILE=qwen3.5-4b-instruct.tokenizer.json \
|
||||
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-4B/resolve/main/tokenizer.json \
|
||||
docker compose -f deploy/docker-compose.yml --profile llm run --rm llm-model-init
|
||||
```
|
||||
|
||||
## 3. Запуск профиля
|
||||
|
||||
CPU (уровни `min`/`medium`, пресеты 3/4):
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml \
|
||||
--profile media --profile transcribe --profile llm up -d
|
||||
```
|
||||
|
||||
GPU (уровень `max`, пресет 5 — GPU обязателен):
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml \
|
||||
--profile media --profile transcribe-gpu --profile llm-gpu up -d
|
||||
```
|
||||
|
||||
Проверка готовности (порт `8080` — `llm`, `8081` — `llm-gpu` на хосте;
|
||||
внутри docker-сети оба доступны как `llm:8080`/`llm-gpu:8080`, `llm-gpu`
|
||||
также отвечает под алиасом `llm` — см. комментарий в `deploy/monitoring/prometheus.yml`):
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
# пока модель грузится: {"error":{"code":503,"message":"Loading model",...}}
|
||||
# сервер готов: {"status":"ok"}
|
||||
```
|
||||
|
||||
## 4. Включение провайдера `qwen_local`
|
||||
|
||||
Дефолт репозитория в `config/plugins.yaml` — `summarizer.provider: "null"`
|
||||
(безопасно для dev-окружений без LLM-сервера). Уровень `min` включается по
|
||||
образцу закомментированного примера в `config/plugins.yaml`; уровни
|
||||
`medium`/`max` собираются автоматически из `TIERS` (`backend/services/ai_tiers.py`)
|
||||
при выборе уровня AI в админке — руками их прописывать не нужно (см.
|
||||
`backend/services/instance_settings.py::_apply_tier_overrides`).
|
||||
|
||||
После правки `config/plugins.yaml` (уровень `min`, ручной dev-сценарий)
|
||||
перезапустить `worker`:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml up -d --force-recreate worker
|
||||
```
|
||||
|
||||
## 5. Проверка вручную
|
||||
|
||||
1. Поднять профиль `llm`/`llm-gpu`, дождаться `docker compose ps` → healthy.
|
||||
2. `curl http://localhost:8080/health` → `{"status":"ok"}`.
|
||||
3. Включить уровень AI в админке (или `qwen_local` в `config/plugins.yaml`
|
||||
для ручного dev-сценария уровня `min`).
|
||||
4. Прогнать пайплайн на тестовом сеансе (см. `docs/plugins/summarizer.md`)
|
||||
и убедиться, что `conference_sessions.summary_data` заполняется.
|
||||
|
||||
## 6. Ресурсы, thinking-режим и мониторинг
|
||||
|
||||
- Требования CPU/RAM/GPU по пресетам — `docs/architecture/adr/004-ai-tier-matrix.md`.
|
||||
- `LLAMA_ARG_CTX_SIZE=16384` — с запасом на чанк до 8000 токенов + промпт +
|
||||
вывод (per-tier `max_tokens_map`/`max_tokens_reduce`, ADR-004).
|
||||
- Thinking-режим (семейство Qwen3.5) отключается флагом `LLAMA_ARG_REASONING=off`
|
||||
(актуальная замена `--chat-template-kwargs '{"enable_thinking":false}'` из
|
||||
ADR-004 — та же семантика, но без деприкейшен-варнинга в логе на каждый
|
||||
запуск, проверено через find-docs по `common/arg.cpp` проекта llama.cpp).
|
||||
- `/metrics` (`LLAMA_ARG_ENDPOINT_METRICS=1`) собирает Prometheus, профиль
|
||||
compose `monitoring` — `deploy/monitoring/prometheus.yml`, job `llm`.
|
||||
90
docs/deploy/monitoring.md
Normal file
90
docs/deploy/monitoring.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Мониторинг (Prometheus + Grafana, профиль compose `monitoring`)
|
||||
|
||||
Независимый compose-профиль — можно поднимать вместе
|
||||
с любым пресетом инсталлятора (1–5) или отдельно.
|
||||
|
||||
## 1. Компоненты
|
||||
|
||||
| Сервис | Образ | Порт (хост) | Назначение |
|
||||
|---|---|---|---|
|
||||
| `prometheus` | `prom/prometheus:v3.13.1` | `9090` | сбор и хранение метрик, оценка правил алертинга |
|
||||
| `postgres-exporter` | `quay.io/prometheuscommunity/postgres-exporter:v0.20.1` | — (внутренний) | метрики PostgreSQL |
|
||||
| `redis-exporter` | `oliver006/redis_exporter:v1.87.0-alpine` | — (внутренний) | метрики Redis |
|
||||
| `grafana` | `grafana/grafana:13.1.0` | `3001` (внутри контейнера `3000`) | дашборд «Пайплайны пост-обработки» |
|
||||
|
||||
Файлы: `deploy/monitoring/prometheus.yml`, `deploy/monitoring/alerts.yml`,
|
||||
`deploy/monitoring/grafana/provisioning/` (datasource + провайдер
|
||||
дашбордов), `deploy/monitoring/grafana/dashboards/pipelines.json`.
|
||||
|
||||
## 2. Запуск
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml --profile monitoring up -d
|
||||
```
|
||||
|
||||
- Prometheus: http://localhost:9090
|
||||
- Grafana: http://localhost:3001 (логин/пароль — `.env`,
|
||||
`GRAFANA_ADMIN_USER`/`GRAFANA_ADMIN_PASSWORD`; `install.sh` генерирует
|
||||
пароль при первой установке)
|
||||
|
||||
Дашборд «Пайплайны пост-обработки» (папка VidConf в Grafana) появляется
|
||||
сразу — источник данных и дашборд провижинятся из файлов, без ручной
|
||||
настройки.
|
||||
|
||||
## 3. Метрики backend
|
||||
|
||||
`GET /metrics` (`backend/api/metrics.py`, без авторизации внутри
|
||||
приложения — снаружи периметра закрыт явным `return 403` в
|
||||
`deploy/nginx/nginx.conf`; Prometheus ходит в backend напрямую по docker-сети,
|
||||
`backend:8000/metrics`, минуя nginx):
|
||||
|
||||
- `vidconf_http_request_duration_seconds` (histogram, `method`/`path`/`status`) —
|
||||
латентность HTTP по шаблону маршрута.
|
||||
- `vidconf_pipeline_sessions` (gauge, `status`) — число сеансов конференций в
|
||||
каждом статусе `pipeline_status`
|
||||
(recording→transcribing→summarizing→notified|failed).
|
||||
- `vidconf_celery_queue_depth` (gauge, `queue`) — глубина очередей Celery
|
||||
(`transcription`/`summarize`/`notify`/`celery`, redis `LLEN`), карта
|
||||
очередей — `docs/deploy/scaling.md`.
|
||||
|
||||
Job `llm` в `prometheus.yml` скрейпит `llm:8080/metrics`
|
||||
(`LLAMA_ARG_ENDPOINT_METRICS=1`) — этот адрес резолвится ЛИБО сервисом
|
||||
`llm` (CPU, профиль `llm`), ЛИБО `llm-gpu` (у него есть сетевой алиас `llm`,
|
||||
см. `deploy/docker-compose.yml`) — профили `llm`/`llm-gpu` взаимоисключающи
|
||||
по пресету, поэтому один job без дублирования.
|
||||
|
||||
## 4. Алерты (`deploy/monitoring/alerts.yml`)
|
||||
|
||||
| Алерт | Условие | severity |
|
||||
|---|---|---|
|
||||
| `PipelineFailed` | рост числа сеансов в статусе `failed` за 15 минут | critical |
|
||||
| `QueueGrowing` | глубина очереди растёт 15 минут подряд и превышает 10 задач | warning |
|
||||
| `LlmDown` | `up{job="llm"} == 0` дольше 2 минут | critical |
|
||||
|
||||
`LlmDown` актуален только на инсталляциях с профилем `llm`/`llm-gpu`
|
||||
(пресеты 3–5) — на пресетах 1/2 (без AI) таргет `llm:8080` в принципе не
|
||||
резолвится и алерт будет постоянно активен, если профиль `monitoring`
|
||||
включён без AI-профиля; в таком случае правило можно закомментировать в
|
||||
локальной копии `alerts.yml`.
|
||||
|
||||
Проверка — искусственно завалить пайплайн и убедиться, что алерт срабатывает:
|
||||
|
||||
```bash
|
||||
# Стек с профилями media, transcribe, llm, monitoring уже поднят,
|
||||
# идёт активная суммаризация (сеанс в статусе summarizing).
|
||||
docker compose -f deploy/docker-compose.yml stop llm
|
||||
# Подождать > 2 минут → Prometheus (http://localhost:9090/alerts)
|
||||
# должен показать LlmDown в состоянии firing, следом — QueueGrowing
|
||||
# (очередь summarize перестаёт разбираться) и, если сеанс не восстановится
|
||||
# за 15 минут (recover_stuck_summaries переставит задачу, workers/celery_app.py),
|
||||
# PipelineFailed.
|
||||
docker compose -f deploy/docker-compose.yml start llm
|
||||
```
|
||||
|
||||
## 5. Хранение
|
||||
|
||||
`prometheus_data`/`grafana_data` — именованные тома, переживают
|
||||
пересоздание контейнеров. Ретеншен Prometheus — дефолт образа (15 дней);
|
||||
для прод-инсталляций с длинным горизонтом донастраивается флагом
|
||||
`--storage.tsdb.retention.time` (не задан в `deploy/docker-compose.yml` —
|
||||
осознанный dev/small-prod дефолт, донастраивается отдельно при необходимости).
|
||||
294
docs/deploy/quality-tiers.md
Normal file
294
docs/deploy/quality-tiers.md
Normal file
@@ -0,0 +1,294 @@
|
||||
# Оценка качества суммаризации по уровням AI
|
||||
|
||||
Методика и результаты прогона утверждённых промптов суммаризации
|
||||
(`workers/summarizer/prompts/summary_map_ru.txt`,
|
||||
`workers/summarizer/prompts/summary_reduce_ru.txt`) на всех трёх уровнях AI
|
||||
(ADR-004, `docs/architecture/adr/004-ai-tier-matrix.md`). Тексты промптов
|
||||
едины для всех уровней и правятся только осознанным решением команды по
|
||||
результатам прогона на этом корпусе — этот документ и есть тот прогон,
|
||||
базис для будущих итераций.
|
||||
|
||||
## 1. Методика
|
||||
|
||||
- Скрипт: `workers/summarizer/eval/run_tiers.py` — создаёт плагин `QwenLocal`
|
||||
штатной фабрикой (`backend/core/plugins/factory.py::create_summarizer`) из
|
||||
`TierSpec` (`backend/services/ai_tiers.py`) для каждого уровня, прогоняет
|
||||
весь корпус через `Summarizer.summarize()`, пишет результаты в
|
||||
`workers/summarizer/eval/results/<уровень>/<файл>.md` + метаданные прогона
|
||||
`_run_meta.json` (модель, base_url, лимиты токенов, время на файл, статус).
|
||||
- Доступность уровня определяется автоматически: `max` требует GPU
|
||||
(`nvidia-smi` в PATH) — на dev-машине без NVIDIA пропускается сразу; для
|
||||
`min`/`medium` скрипт проверяет `GET /health` LLM-сервера и сверяет
|
||||
реально загруженную модель через `GET /v1/models` (официальный
|
||||
OpenAI-совместимый эндпоинт llama.cpp — id ответа содержит путь к файлу
|
||||
модели) с ожидаемой по `TierSpec`, чтобы не приписать результат не тому
|
||||
уровню (в одной docker-сети `min` и `medium` по умолчанию делят один и тот
|
||||
же compose-сервис `llm`, отличаются только тем, какой `LLM_MODEL_FILE`
|
||||
сервис фактически загрузил).
|
||||
- Промпты скрипт не читает и не редактирует — только передаёт `QwenLocal`
|
||||
абсолютный путь к штатному каталогу `workers/summarizer/prompts/`.
|
||||
Проверка инварианта: `git diff --stat workers/summarizer/prompts/` пуст.
|
||||
- Оценка качества — ручная экспертная по каждому файлу результата, по трём
|
||||
критериям:
|
||||
1. **Полнота тем** — все ключевые темы/решения/цифры транскрипта попали в
|
||||
соответствующие разделы формата, ничего существенного не потеряно на
|
||||
границах чанков (особенно у длинного многотемного транскрипта — 3 чанка
|
||||
и reduce);
|
||||
2. **Галлюцинации** — модель не добавляет фактов, имён, цифр, сроков,
|
||||
которых нет в транскрипте;
|
||||
3. **Следование формату** — ровно 4 раздела (`## Ключевые тезисы`,
|
||||
`## Принятые решения и задачи`, `## Открытые вопросы`, `## Цифры и
|
||||
факты`), пустой раздел — одна строка `—`, без вступлений/заключений.
|
||||
|
||||
## 2. Корпус
|
||||
|
||||
`workers/summarizer/eval/corpus/` — 5 транскриптов на русском в штатном
|
||||
формате входа `summarize` (`[Имя MM:SS] текст`), разного объёма и профиля:
|
||||
|
||||
| Файл | Профиль | Длительность | Спикеров | Особенность для оценки |
|
||||
|---|---|---|---|---|
|
||||
| `01-short-standup.txt` | короткий дейлик | ~10 мин | 3 | один чанк — map без reduce |
|
||||
| `02-long-multitopic.txt` | планирование, 4 темы | ~60 мин | 4 | 3 чанка по 20 мин → map-reduce, объединение дублей и снятие закрытых вопросов между чанками |
|
||||
| `03-dialogue-1on1.txt` | ревью 1-на-1 | ~12 мин | 2 | плотный диалог одних и тех же двух имён |
|
||||
| `04-multispeaker-guest.txt` | демо клиенту | ~14 мин | 3 внутр. + 1 гость | имя гостя из двух слов (`Виктор Соколов`), различение внутренних/внешнего |
|
||||
| `05-metrics-heavy.txt` | квартальный обзор метрик | ~13 мин | 3 | много чисел/процентов/сумм — точность раздела «Цифры и факты» |
|
||||
|
||||
## 3. Результаты: уровень `min` (Qwen3.5-4B-Instruct, Q4_K_M, CPU)
|
||||
|
||||
Прогон на dev-машине (macOS, без NVIDIA GPU) — единственный уровень,
|
||||
доступный локально.
|
||||
|
||||
**Как запускался:**
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml --profile llm up -d \
|
||||
llm-models-init llm-model-init llm
|
||||
# дождаться docker compose ps → llm healthy (LLM_MODEL_FILE не задан в .env
|
||||
# — дефолт download-model.sh совпадает с уровнем min, Qwen3.5-4B)
|
||||
|
||||
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
|
||||
--levels min --base-url http://localhost:8080/v1
|
||||
```
|
||||
|
||||
**Параметры уровня (ADR-004):** `temperature=0.2`, `max_tokens_map=1024`,
|
||||
`max_tokens_reduce=1536`, `chunk_minutes=20`, `CTX_SIZE=16384`.
|
||||
|
||||
Прогон реально выполнен (`workers/summarizer/eval/results/min/`, метаданные —
|
||||
`_run_meta.json`, отдельные результаты — `<файл>.md`). Первая попытка
|
||||
прогона всего корпуса словила инфраструктурный сбой — LLM-контейнер `llm`
|
||||
был убит хостом (`exit 137`, SIGKILL) на пятом файле: Docker Desktop на этой
|
||||
машине выделяет VM всего 7,75 ГиБ (`docker info`), из них ~3,4 ГиБ уже
|
||||
занимал сам `llm` (веса 2,7 ГБ + KV-кэш на `CTX_SIZE=16384`), а ещё ~1 ГиБ —
|
||||
параллельно работающий dev-стек (`postgres`/`redis`/`livekit`/`coturn`/
|
||||
`backend-manual`); после `docker compose ... up -d llm` (перезапуск) второй
|
||||
прогон всего корпуса прошёл 5/5 без сбоев. Это не проблема плагина/промптов,
|
||||
а сайзинг хоста — ADR-004 закладывает под пресет 3 (`min`) минимум 16 ГБ RAM
|
||||
именно ПОД ЭТОТ уровень, не под уровень поверх произвольного количества
|
||||
параллельных dev-контейнеров; отмечено как риск в разделе 6.
|
||||
|
||||
**Длительность прогона (второй, чистый запуск, 5/5 успешно):**
|
||||
|
||||
| Файл | Длительность записи | Чанков (map/reduce) | Время прогона |
|
||||
|---|---|---|---|
|
||||
| `01-short-standup.txt` | ~10 мин | 1 (без reduce) | 52,6 с |
|
||||
| `02-long-multitopic.txt` | ~60 мин | 3 + reduce | 479,6 с (~8 мин) |
|
||||
| `03-dialogue-1on1.txt` | ~12 мин | 1 (без reduce) | 65,9 с |
|
||||
| `04-multispeaker-guest.txt` | ~14 мин | 1 (без reduce) | 76,7 с |
|
||||
| `05-metrics-heavy.txt` | ~13 мин | 1 (без reduce) | 114,5 с |
|
||||
|
||||
Ориентир ADR-004 «часовой транскрипт на CPU ≈ 6,5 мин» — реально получено
|
||||
~8 мин на `02-long-multitopic.txt` (3 map-вызова + 1 reduce), в пределах
|
||||
разумного расхождения для другого CPU и разделяемого с dev-стеком хоста.
|
||||
|
||||
**Экспертная оценка по корпусу (полнота / галлюцинации / формат):**
|
||||
|
||||
- **Формат** — соблюдён на 5/5: ровно 4 раздела с точными заголовками, без
|
||||
вступлений и заключений, ни одного нарушения структуры вывода.
|
||||
- **Цифры и факты** — очень высокая точность: на `05-metrics-heavy.txt`
|
||||
(стресс-тест на числа) сверены вручную ВСЕ 19 пунктов раздела «Цифры и
|
||||
факты» с исходным транскриптом — ни одного искажённого или выдуманного
|
||||
числа. На `02-long-multitopic.txt` (map-reduce, 3 чанка) все денежные
|
||||
суммы, проценты и сроки из раздела тоже подтвердились дословно.
|
||||
- **Галлюцинации фактов** — единственный найденный случай: в
|
||||
`04-multispeaker-guest.txt` раздел «Ключевые тезисы» смешал два факта —
|
||||
в транскрипте отложена в релиз 0.1.0 только ЗАПИСЬ конференций
|
||||
(«Запись — в базовом релизе только анонс...»), а саммари в тезисах
|
||||
сформулировало это как отложенные «запись встреч И генерации саммари»,
|
||||
хотя суммаризация в демо явно показана как уже работающая функция
|
||||
(«Работают, покажу на примере...», тот же транскрипт). При этом раздел
|
||||
«Цифры и факты» ТОГО ЖЕ файла формулирует факт верно («Релиз с функцией
|
||||
записи запланирован на 0.1.0») — то есть внутри одного вывода модель сама
|
||||
себе противоречит между разделами.
|
||||
- **Незакрытые вопросы, на которые ответ уже прозвучал** — систематическая
|
||||
проблема, найдена в 2 из 5 файлов:
|
||||
- `01-short-standup.txt` (один чанк, без reduce): вопрос Ирины «кто-нибудь
|
||||
смотрел баг про дубли уведомлений» остался в «Открытых вопросах», хотя
|
||||
Павел в том же транскрипте на него ответил («Я смотрел, там
|
||||
идемпотентность сломана...»). Причина — у map-промпта НЕТ инструкции
|
||||
снимать вопрос, отвеченный в том же фрагменте (только у reduce-промпта
|
||||
есть инструкция снимать вопросы, закрытые МЕЖДУ фрагментами) — это
|
||||
структурный пробел самого промпта, не только слабость модели.
|
||||
- `02-long-multitopic.txt` (map-reduce, 3 чанка): ДВА вопроса остались в
|
||||
«Открытых», хотя реально были закрыты в более позднем чанке — «кто
|
||||
будет проводить техническое интервью» (позже: «назначаю тебя» Игорю) и
|
||||
«сколько часов записей на 250 ГБ» (позже: «примерно 200 часов»). Здесь
|
||||
reduce-промпт ЯВНО требует «если вопрос... был закрыт в позднем —
|
||||
оставь только итоговое состояние», но 4B-модель это правило не
|
||||
применила — прямое подтверждение вывода ADR-004: `min` работает
|
||||
на пределе инструктивной сложности, к reduce это относится сильнее, чем
|
||||
к map.
|
||||
- **Атрибуция ответственных при reduce** — 1 случай неверного приписывания
|
||||
автора решения не тому спикеру: пункт «Добавить в заявку резервный SSD на
|
||||
1 ТБ для бэкапов базы» приписан Марине, хотя в транскрипте это сказала
|
||||
Дарья («Ещё добавлю в заявку резервный SSD...»); аналогично пункт «Завести
|
||||
задачу на тестовое восстановление бэкапа» приписан Марине, хотя в
|
||||
транскрипте это её ПОРУЧЕНИЕ Игорю, а исполнитель — Игорь (что видно по
|
||||
соседнему, отдельно возникшему пункту «Выполнить настройку тестового
|
||||
восстановления... — ответственный: Игорь» — то есть один и тот же пункт
|
||||
задвоился на два с разной атрибуцией вместо объединения дублей, как
|
||||
требует reduce-промпт).
|
||||
- **Спутывание завершённого действия с будущей задачей** — 2 случая в
|
||||
`05-metrics-heavy.txt`: фразы Романа и Артёма о том, что действие УЖЕ
|
||||
сделано в прошлом («обновил документ вчера», «начиная с прошлой недели
|
||||
лимит reduce минимум 1536 токенов» — прошедшее время) модель занесла в
|
||||
«Принятые решения и задачи» как будто это предстоящие задачи, с явно
|
||||
нелогичным полем «срок: вчера» в одном из пунктов.
|
||||
- **Потеря точного срока в пользу обобщения** — в `04-multispeaker-guest.txt`
|
||||
конкретный срок «к среде» (когда гость пришлёт список сотрудников)
|
||||
переформулирован в размытое «до начала пилота», а в поле «срок» пункта
|
||||
указано «не указан», хотя конкретная дата в транскрипте была.
|
||||
|
||||
**Вывод по `min`:** формат и числовая точность — сильная сторона уже на
|
||||
самой маленькой модели уровня; систематическая слабость — согласованность
|
||||
между разделами и через reduce (незакрытые вопросы, задвоенные решения,
|
||||
неверная атрибуция, путаница «сделано» vs «сделать»). Это ожидаемо и
|
||||
согласуется с выводом ADR-004 («Qwen ~3B/4B на пределе инструктивной
|
||||
сложности», сильнее всего проявляется на reduce) — рекомендация: НЕ трогать
|
||||
промпты по этим находкам (правка промптов — отдельное осознанное решение с
|
||||
повторным прогоном корпуса после правки, не автоматическая реакция на
|
||||
единичный прогон), ожидать улучшения именно от роста модели на
|
||||
`medium`/`max` и сравнить те же файлы/те же найденные проблемы после
|
||||
прогона на этих уровнях (раздел 6).
|
||||
|
||||
## 4. Результаты: уровень `medium` (Qwen3.5-9B-Instruct, Q4_K_M)
|
||||
|
||||
Не запускался на этой машине — на dev-хосте (macOS, без NVIDIA GPU) для
|
||||
`medium` поднят тот же CPU-сервис `llm`, что и для `min` (оба используют
|
||||
`base_url: http://llm:8080/v1`, ADR-004); чтобы прогнать `medium`, нужен
|
||||
СВОЙ сервер с моделью Qwen3.5-9B — либо второй `llm`-сервис на другом
|
||||
порту/томе с `LLM_MODEL_FILE=qwen3.5-9b-instruct-q4_k_m.gguf`, либо GPU-хост.
|
||||
|
||||
**Ручной шаг (на CPU-хосте с ≥32 ГБ RAM или GPU-хосте):**
|
||||
|
||||
```bash
|
||||
LLM_MODEL_FILE=qwen3.5-9b-instruct-q4_k_m.gguf \
|
||||
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-9B-GGUF/resolve/main/Qwen3.5-9B-Q4_K_M.gguf \
|
||||
LLM_TOKENIZER_FILE=qwen3.5-9b-instruct.tokenizer.json \
|
||||
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-9B/resolve/main/tokenizer.json \
|
||||
docker compose -f deploy/docker-compose.yml --profile llm up -d
|
||||
|
||||
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
|
||||
--levels medium --base-url http://localhost:8080/v1
|
||||
```
|
||||
|
||||
`run_tiers.py` сверит загруженную модель через `/v1/models` и откажется
|
||||
писать результат в каталог `medium`, если сервер поднят с другой моделью —
|
||||
скрипт сам подскажет, что не так.
|
||||
|
||||
<!-- TODO: результаты после ручного прогона на GPU/большом CPU-хосте. -->
|
||||
|
||||
## 5. Результаты: уровень `max` (Qwen3.5-35B-A3B-Instruct MoE, Q4_K_M, GPU)
|
||||
|
||||
Не запускался — `run_tiers.py` пропускает `max` автоматически на этой
|
||||
машине (`nvidia-smi` не найден), GPU обязателен (ADR-004, требование ≥16 ГБ
|
||||
VRAM, рекомендовано 24 ГБ).
|
||||
|
||||
**Ручной шаг (на GPU-хосте):**
|
||||
|
||||
```bash
|
||||
LLM_MODEL_FILE=qwen3.5-35b-a3b-instruct-q4_k_m.gguf \
|
||||
LLM_MODEL_URL=https://huggingface.co/unsloth/Qwen3.5-35B-A3B-GGUF/resolve/main/Qwen3.5-35B-A3B-Q4_K_M.gguf \
|
||||
LLM_TOKENIZER_FILE=qwen3.5-35b-a3b-instruct.tokenizer.json \
|
||||
LLM_TOKENIZER_URL=https://huggingface.co/Qwen/Qwen3.5-35B-A3B/resolve/main/tokenizer.json \
|
||||
docker compose -f deploy/docker-compose.yml --profile llm-gpu up -d
|
||||
|
||||
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py \
|
||||
--levels max --base-url http://localhost:8081/v1
|
||||
```
|
||||
|
||||
(порт `8081` — хостовый порт `llm-gpu` в `deploy/docker-compose.yml`, см.
|
||||
`docs/deploy/llm-setup.md`, раздел 3).
|
||||
|
||||
<!-- TODO: результаты после ручного прогона на GPU-хосте. -->
|
||||
|
||||
## 6. Наблюдения и рекомендации
|
||||
|
||||
По результатам реального прогона `min` (раздел 3; `medium`/`max` — пока
|
||||
только методика, реальных данных нет, см. разделы 4–5):
|
||||
|
||||
1. **Полнота по типам транскриптов.** Короткие однотемные записи
|
||||
(`01`, `03`) и однотемные записи среднего размера с одним чанком
|
||||
(`04`, `05`) обрабатываются полнее и надёжнее, чем длинный многотемный
|
||||
транскрипт с map-reduce (`02`) — там, где нужен reduce, у 4B-модели
|
||||
заметно чаще не срабатывают инструкции промпта про снятие закрытых
|
||||
вопросов и объединение дублей (раздел 3). Диалог с цифрами (`05`) и
|
||||
гостем (`04`) сами по себе не оказались сложнее для `min` — проблема
|
||||
именно в reduce-стадии, а не в теме/числе спикеров.
|
||||
2. **Систематических галлюцинаций фактов/цифр не найдено** — числа
|
||||
воспроизводятся дословно даже в «числовом» стресс-тесте (`05`, все 19
|
||||
пунктов сверены вручную). Единственная найденная смысловая ошибка —
|
||||
конфляция двух разных фактов в одном разделе одного файла (`04`), не
|
||||
выдумывание нового факта из ничего.
|
||||
3. **Систематическая, а не случайная проблема** — это несогласованность
|
||||
между разделами и через reduce: незакрытые вопросы, на которые уже
|
||||
прозвучал ответ (2 из 5 файлов, включая случай БЕЗ reduce — структурный
|
||||
пробел в самом map-промпте, не только слабость модели), задвоенные
|
||||
решения с разной атрибуцией ответственного при reduce (1 файл), и
|
||||
путаница «уже сделано» / «предстоит сделать» (2 случая в одном файле).
|
||||
4. **Длительность на CPU** совпадает по порядку величины с ориентиром
|
||||
ADR-004 («часовой транскрипт ≈ 6,5 мин»): реально получено ~8 мин на
|
||||
`02-long-multitopic.txt` (3 map + 1 reduce). Расхождение объясняется
|
||||
разделяемым с dev-стеком хостом, не архитектурной проблемой.
|
||||
5. **Инфраструктурный риск, не связанный с моделью/промптами:** на dev-
|
||||
машине (Docker Desktop, VM 7,75 ГиБ) `llm`-контейнер был убит хостом
|
||||
(OOM на уровне VM, не cgroup — `OOMKilled: false`, но `exit 137`)
|
||||
при параллельной работе полного dev-стека. ADR-004 требует 16 ГБ RAM
|
||||
под пресет 3 — это бюджет ПОД уровень `min`, не поверх производного
|
||||
dev-окружения с БД/Redis/LiveKit/Coturn/др. Рекомендация: в
|
||||
`docs/deploy/install.md`/чек-листе явно указывать, что 16 ГБ — это
|
||||
помимо памяти, занятой остальным dev/prod-стеком, если он совмещён на
|
||||
одной машине.
|
||||
|
||||
**Рекомендация по промптам:** правки текстов промптов НЕ
|
||||
делались и не рекомендуются по итогам этого прогона в одностороннем
|
||||
порядке. Найденные проблемы (пп. 3) касаются reduce-инструкций, которые в
|
||||
промпте УЖЕ явно прописаны («если вопрос... был закрыт в позднем — оставь
|
||||
только итоговое состояние», «объединяй дубли») — модель `min` (4B) их не
|
||||
всегда выполняет, что согласуется с выводом ADR-004 о пределе
|
||||
инструктивной сложности на этом размере. Ожидаемая гипотеза (проверяется
|
||||
прогоном `medium`/`max` на ЭТОМ ЖЕ корпусе после того, как появится
|
||||
GPU/большой CPU-хост, разделы 4–5): более крупная модель должна снять
|
||||
именно эти reduce-ошибки без изменения текста промпта. Если после прогона
|
||||
`medium`/`max` те же ошибки останутся систематическими и на большей модели —
|
||||
это станет основанием для отдельного осознанного решения команды о правке
|
||||
промптов, с повторным прогоном этого же корпуса до и после правки.
|
||||
|
||||
## Как повторить и добавить уровень
|
||||
|
||||
```bash
|
||||
cd backend && uv run python ../workers/summarizer/eval/run_tiers.py --help
|
||||
```
|
||||
|
||||
- `--levels min,medium,max` — какие уровни пробовать (недоступные — пропуск
|
||||
с причиной в stdout, скрипт не падает).
|
||||
- `--base-url` — переопределить адрес LLM-сервера для ВСЕХ уровней (нужно
|
||||
при запуске с хоста вне docker-сети — штатные `http://llm:8080/v1`/
|
||||
`http://llm-gpu:8080/v1` из `backend/services/ai_tiers.py` резолвятся
|
||||
только внутри неё).
|
||||
- Результаты и `_run_meta.json` — в `workers/summarizer/eval/results/`,
|
||||
перезаписываются при повторном прогоне того же уровня.
|
||||
|
||||
## Смотрите также
|
||||
|
||||
- ADR-004 — `docs/architecture/adr/004-ai-tier-matrix.md`.
|
||||
- `docs/deploy/llm-setup.md` — установка LLM-сервера по уровням.
|
||||
146
docs/deploy/scaling.md
Normal file
146
docs/deploy/scaling.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Горизонтальное масштабирование очередей Celery
|
||||
|
||||
Описывает, как развести обработку задач по отдельным
|
||||
репликам/сервисам под нагрузкой, когда одного контейнера `worker`
|
||||
недостаточно. Здесь — инструкция для оператора, применимая к текущей и
|
||||
будущей конфигурации `deploy/docker-compose.yml`.
|
||||
|
||||
## 1. Карта очередей
|
||||
|
||||
Маршрутизация задач по очередям задана в `workers/celery_app.py`
|
||||
(`app.conf.task_routes`) — приоритет между семьями задач реализован
|
||||
**изоляцией очередей**, а не Redis-priorities Celery (транспорт `redis`
|
||||
эмулирует приоритеты ненадёжно, без строгих гарантий порядка — в отличие от
|
||||
RabbitMQ).
|
||||
|
||||
| Очередь | Задачи | Кто слушает по умолчанию |
|
||||
|----------------|-------------------------------------------------------------------------|---------------------------|
|
||||
| `transcription`| `run_pipeline` (faster-whisper) | `worker-transcriber` (профиль `transcribe`), `--pool=solo` — ctranslate2/faster-whisper несовместимы с prefork |
|
||||
| `summarize` | `summarize_session` (Qwen map-reduce) | базовый `worker` |
|
||||
| `notify` | `notify_session`, `send_invitations` (.ics-приглашения) | базовый `worker` |
|
||||
| `celery` (default) | `cleanup_conferences`, `recover_stuck_summaries`, `recover_stuck_notifications` (маршрута не имеют, обслуживание) | базовый `worker` |
|
||||
|
||||
Базовый сервис `worker` слушает `celery,summarize,notify` — для малых
|
||||
пресетов поставки (1–3) это один контейнер, обрабатывающий и суммаризацию, и
|
||||
уведомления, и обслуживание. `worker-transcriber` — всегда отдельный процесс
|
||||
(независимо от пресета), т.к. пул `solo` несовместим с остальными задачами
|
||||
в том же процессе.
|
||||
|
||||
Глубину каждой очереди в реальном времени видно в `GET /metrics`
|
||||
(`vidconf_celery_queue_depth{queue=...}`, `backend/api/metrics.py`) и в
|
||||
Grafana-дашборде «Пайплайны пост-обработки» (алерт `QueueGrowing`,
|
||||
`deploy/monitoring/alerts.yml`) — по этим показателям и принимается решение
|
||||
о вынесении очереди в отдельную реплику.
|
||||
|
||||
## 2. Общее правило: `celery beat` — только один экземпляр
|
||||
|
||||
Периодические задачи (`app.conf.beat_schedule`) планирует `celery beat`.
|
||||
Базовый `worker` запускается с флагом `-B` (worker + встроенный beat в одном
|
||||
процессе, `deploy/docker-compose.yml`). При масштабировании **нельзя** просто
|
||||
поднять несколько реплик сервиса с `-B` — каждая реплика завела бы
|
||||
собственный планировщик, и periodic-задачи (`cleanup_conferences`,
|
||||
`recover_stuck_*`) ставились бы в очередь многократно на каждом тике.
|
||||
|
||||
Правило: ровно один процесс во всей инсталляции запускается с `-B`
|
||||
(встроенным или отдельным `celery -A workers.celery_app beat`); все
|
||||
дополнительные реплики — только `worker` без `-B`, с явным `-Q` на нужные
|
||||
очереди.
|
||||
|
||||
## 3. Вынос очереди в отдельную реплику
|
||||
|
||||
Пример: суммаризация (`summarize`) стала узким местом — очередь растёт,
|
||||
базовый `worker` не успевает. Решение — отдельный сервис-потребитель только
|
||||
этой очереди, без встроенного beat (он остаётся на базовом `worker`).
|
||||
|
||||
Добавить в `deploy/docker-compose.override.yml` (или новый профиль по
|
||||
аналогии с `worker-transcriber`):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
worker-summarize:
|
||||
build:
|
||||
context: ../backend
|
||||
dockerfile: Dockerfile
|
||||
restart: unless-stopped
|
||||
# Без -B: beat уже запущен на базовом worker (см. правило выше).
|
||||
command: ["uv", "run", "celery", "-A", "workers.celery_app", "worker",
|
||||
"-Q", "summarize", "--hostname=worker-summarize-%h@%h", "--loglevel=info"]
|
||||
env_file:
|
||||
- ../.env
|
||||
environment:
|
||||
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
||||
PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml}
|
||||
PYTHONPATH: /app
|
||||
volumes:
|
||||
- ../workers:/app/workers:ro
|
||||
- ../config:/app/config:ro
|
||||
- llm-models:/models/qwen:ro
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
и одновременно убрать `summarize` из списка очередей базового `worker`
|
||||
(его команда сужается до `-Q celery,notify`), чтобы задачи не выполнялись
|
||||
дважды разными процессами (Celery доставляет задачу ровно одному
|
||||
consumer'у одной и той же очереди — дублирования не будет, но держать
|
||||
лишний неиспользуемый consumer незачем).
|
||||
|
||||
Аналогично можно выделить `notify` в `worker-notify` (та же схема,
|
||||
`-Q notify`) — например, если рассылка приглашений/уведомлений на большую
|
||||
аудиторию (SMTP-латентность) начинает задерживать саммаризацию соседних
|
||||
сеансов при их совместном обслуживании базовым `worker`.
|
||||
|
||||
## 4. Масштабирование реплик через `docker compose up --scale`
|
||||
|
||||
Если одной выделенной очереди тоже мало (несколько ядер CPU для
|
||||
суммаризации/уведомлений), реплицируем сервис командой `--scale`:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml -f deploy/docker-compose.override.yml \
|
||||
up -d --scale worker-summarize=3
|
||||
```
|
||||
|
||||
Требования для корректного масштабирования сервиса:
|
||||
|
||||
1. **Без `-B`** на масштабируемом сервисе (правило §2).
|
||||
2. **Без фиксированного `--hostname`** на весь сервис — при нескольких
|
||||
репликах одинаковое имя узла Celery приведёт к конфликту регистрации в
|
||||
кластере (соединения будут путаться, `celery inspect` начнёт видеть
|
||||
произвольного из реплик). Использовать `%h` (имя контейнера, уникальное
|
||||
у каждой реплики Compose) — см. `--hostname=worker-summarize-%h@%h` в
|
||||
примере выше. Это же ограничение действует и для `worker-transcriber`:
|
||||
его текущий healthcheck (`--destination worker-transcriber@localhost`)
|
||||
жёстко завязан на единственную реплику; при `--scale worker-transcriber=N`
|
||||
healthcheck и `--hostname` в `deploy/docker-compose.yml` потребуется
|
||||
поменять на шаблон `%h` (и убрать `--destination`, либо адресовать
|
||||
каждую реплику отдельно) — вне рамок этого документа, т.к. правит
|
||||
основной `deploy/docker-compose.yml` (devops-часть).
|
||||
3. **Без `container_name`** и без фиксированных host-портов на
|
||||
масштабируемом сервисе (у Celery-воркеров портов нет — ограничение не
|
||||
актуально для `worker*`, но актуально, если аналогичный приём
|
||||
применяется к `backend` за `nginx upstream`).
|
||||
4. Ресурсные лимиты (`cpus`/`mem_limit`) в Compose-файле применяются к
|
||||
КАЖДОЙ реплике, а не разделяются между ними — планировать суммарное
|
||||
потребление хоста (`N × cpus`).
|
||||
|
||||
Для `worker-transcriber` тот же приём уже применим на уровне профиля:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.yml --profile transcribe \
|
||||
up -d --scale worker-transcriber=2
|
||||
```
|
||||
|
||||
(после снятия ограничения фиксированного `--hostname`, см. пункт 2 выше).
|
||||
|
||||
## 5. Когда масштабировать
|
||||
|
||||
Ориентир — глубина очереди (`vidconf_celery_queue_depth`) растущая дольше
|
||||
15 минут (алерт `QueueGrowing`, `deploy/monitoring/alerts.yml`) либо
|
||||
устойчиво положительная под обычной нагрузкой инстанса. Для `summarize` и
|
||||
`transcription` также ориентир — доля CPU/GPU (транскрибация и
|
||||
суммаризация — тяжёлые по вычислениям шаги, `docs/deploy/hardware-profiles.md`);
|
||||
`notify` почти всегда I/O-bound (SMTP) — реплики полезны в первую очередь
|
||||
при большой аудитории рассылок (закреплённые конференции с длинным списком
|
||||
участников/приглашённых).
|
||||
Reference in New Issue
Block a user