Files
2026-07-17 08:15:07 +00:00

257 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# meeting-assistant
Локальная система записи, транскрибации и рассылки итогов встреч.
Текущий этап: **Этап 3. Авторизация организатора и защищённое создание встречи**.
## Планируемый стек
- Backend: FastAPI, SQLAlchemy 2, Alembic, PostgreSQL, Redis, RQ
- Frontend: React, TypeScript, Vite
- Встречи и запись: Jitsi Meet, Jibri
- Локальный AI: faster-whisper/MLX Whisper, Ollama/Gemma
- Рассылка: n8n/SMTP
## Структура
```text
backend/
frontend/
infrastructure/
data/
recordings/
audio/
transcripts/
reports/
docs/
```
## PostgreSQL
PostgreSQL теперь является частью проекта. При Docker-развёртывании сервис `postgres` создаёт пустую базу, `migrator` применяет Alembic-миграции, затем `seeder` добавляет начальные справочники до запуска backend.
Backend берёт подключение из `DATABASE_URL`:
```env
DATABASE_URL=postgresql+psycopg://meeting:password@postgres:5432/meeting_assistant
```
Схема создаётся первой миграцией `backend/alembic/versions/20260714_0001_initial_schema.py` и включает таблицы из ТЗ:
- `departments`
- `department_recipients`
- `meeting_types`
- `meetings`
- `meeting_recipients`
- `processing_events`
- `users`
Начальные данные добавляются модулем `backend/app/seed.py`:
- отделы: `ksb`, `sales`, `marketing`, `ved`, `trade-operations`
- служебный тип встречи: `default`
- получатели отделов: `ksb@osk-group.ru`, `rop@osk-group.ru`, `marketing3@osk-group.ru`,
`Ved2@osk-group.ru`, `Assistved2@osk-group.ru`, `bai@osk-group.ru`
## Backend
Backend пока содержит минимальное FastAPI-приложение с endpoint:
```text
GET /health
GET /health/db
POST /api/auth/login
GET /api/auth/me
GET /api/departments
GET /api/departments/{id}/recipients
GET /api/meeting-types
POST /api/meetings
GET /api/meetings/{id}
GET /api/meetings/{id}/status
POST /api/meetings/{id}/recording/request-start
POST /api/meetings/{id}/recording/confirmed
POST /api/meetings/{id}/recording/stopped
GET /api/meetings/{id}/preview
PUT /api/meetings/{id}/preview
POST /api/meetings/{id}/send
POST /api/internal/recordings/finalized
POST /api/internal/email-delivery
```
Пример создания встречи:
```json
{
"department_id": 1,
"meeting_type_id": 1,
"title": "КСБ",
"organizer_email": "organizer@example.local"
}
```
`POST /api/meetings` создаёт уникальный `room_name` и сохраняет снимок активных получателей отдела в `meeting_recipients`.
`GET /api/meetings/{id}` возвращает карточку встречи со статусом, room name и сохранёнными получателями.
`GET /api/meetings/{id}/status` возвращает компактный статус обработки: `status`, `progress`, ошибки и пути к артефактам.
Recording endpoints фиксируют контур серверной записи: `created -> recording_starting -> recording -> recording_finalizing`.
Internal endpoint для Jibri переводит встречу в `recorded` и сохраняет путь к файлу записи:
```bash
curl -X POST http://localhost:8000/api/internal/recordings/finalized \
-H "Content-Type: application/json" \
-H "X-Internal-Token: change-me" \
-d "{\"meeting_id\":\"<uuid>\",\"recording_path\":\"/data/recordings/example.mp4\"}"
```
Готовый пример Jibri finalize-скрипта находится в `infrastructure/jibri/finalize.sh`. Его нужно подключить на стороне Jibri вместе с переменными `BACKEND_INTERNAL_URL`, `PROCESSING_TOKEN` и `RECORDING_PATH`. `MEETING_ID` можно передать явно, но для реального Jibri-callback достаточно `ROOM_NAME`, metadata записи или пути записи, из которого скрипт выведет `room_name`. Backend принимает только пути внутри `RECORDINGS_ROOT`.
Processing worker после статуса `recorded` извлекает через FFmpeg mono PCM WAV 16 kHz, запускает настроенный Whisper engine и сохраняет два артефакта:
- `transcript_path` - `transcript.txt` с полным текстом;
- `transcript_json_path` - `transcript.json` со структурой `{ "full_text": "...", "segments": [{ "start": 0.0, "end": 1.5, "text": "..." }] }`.
Исходный файл Jibri используется только как временный источник аудиодорожки. В письма и n8n payload путь к видео не передаётся; рассылка содержит протокол и, если разрешено получателю, стенограмму. Отдельный cleanup-процесс удаляет исходные видео старше `RECORDING_VIDEO_RETENTION_DAYS` дней, по умолчанию 30, и очищает `meetings.recording_path`. В Docker Compose это сервис `recording-cleanup`; вручную его можно запустить так:
```bash
cd backend
.venv\Scripts\python -m app.recording_cleanup
```
Для Mac Studio основной режим - `mlx-whisper` на Apple Silicon; `faster-whisper` остаётся fallback для CPU/Linux/NVIDIA. FFmpeg нужно ставить там же, рядом с RQ worker-ом. Инструкция: `infrastructure/worker-mac/README.md`.
Ollama/Gemma поднимается сервисом `ollama` внутри Docker Compose без публикации порта наружу. Worker обращается к нему по внутреннему адресу `http://ollama:11434`. После первого запуска нужно загрузить модель:
```bash
cd infrastructure
docker compose exec ollama ollama pull gemma3:12b
```
Модель выбирается через `OLLAMA_MODEL`, максимальный размер части стенограммы для chunking - через `OLLAMA_CHUNK_MAX_CHARS`.
Если Gemma/Ollama нужно вынести на Mac Studio, готовый full deploy находится в `infrastructure/ollama-mac`. Там есть launchd-сервис, загрузка модели, health-check и инструкция, как связать Mac Ollama с processing worker.
Рассылка выполняется через n8n, а не через локальный SMTP-скрипт. По умолчанию `AUTO_SEND_EMAILS=true`, поэтому после подготовки протокола встреча с `auto_send=true` автоматически переводится в `sending` и отправляет `POST` на `N8N_WEBHOOK_URL` с заголовками `X-N8N-Webhook-Secret` и `X-Idempotency-Key`. Payload содержит раздельные группы `to`/`cc`/`bcc` и массив `deliveries`; в каждой delivery уже лежат только те материалы, которые разрешены конкретному получателю. n8n после фактической доставки должен вызвать `POST /api/internal/email-delivery` с тем же `X-N8N-Webhook-Secret`, чтобы backend обновил `meeting_recipients.delivery_status` и перевёл встречу в `completed` или `failed`. Endpoint `POST /api/meetings/{id}/send` остаётся для ручного повторного запуска/административных сценариев.
`POST /api/meetings` требует Bearer token. Email организатора берётся из авторизованного пользователя, а не из формы.
Начальный пользователь создаётся seed-скриптом из переменных:
```env
INITIAL_ADMIN_EMAIL=admin@example.local
INITIAL_ADMIN_PASSWORD=change-me-admin
INITIAL_ADMIN_NAME=Администратор
```
Локальный запуск после установки зависимостей:
```bash
cd backend
python -m venv .venv
.venv\Scripts\pip install -r requirements-dev.txt
.venv\Scripts\uvicorn app.main:app --reload
```
Проверка:
```bash
cd backend
.venv\Scripts\python -m ruff check .
.venv\Scripts\python -m ruff format --check .
.venv\Scripts\python -m mypy
.venv\Scripts\pytest
```
Docker-запуск PostgreSQL + миграции + backend + frontend + Redis:
```bash
copy .env.example .env
docker compose -f infrastructure/docker-compose.yml up --build
```
Проверка:
```bash
curl http://localhost:8000/health
curl http://localhost:8000/health/db
```
`GET /health/db` выполняет короткий `SELECT 1`. Если PostgreSQL недоступна, endpoint вернёт `503`.
Alembic находится в `backend/alembic`. Конфигурация берёт URL из `DATABASE_URL` через `backend/alembic/env.py`.
Создание новой ревизии, когда модели уже описаны:
```bash
cd backend
.venv\Scripts\python -m alembic revision --autogenerate -m "describe change"
```
Ручное применение миграций:
```bash
cd backend
.venv\Scripts\python -m alembic upgrade head
```
Ручное заполнение начальных справочников:
```bash
cd backend
.venv\Scripts\python -m app.seed
```
## Frontend
Frontend содержит React + TypeScript + Vite экран подготовки встречи. Он читает справочники из backend:
- вход организатора: `POST /api/auth/login`
- отделы: `GET /api/departments`
- получатели выбранного отдела: `GET /api/departments/{id}/recipients`
- типы встреч: `GET /api/meeting-types`
- создание встречи: `POST /api/meetings`
После создания встречи frontend показывает `room_name` и ссылку на корпоративный Jitsi из `VITE_JITSI_BASE_URL`. Авторизация в Jitsi остаётся на стороне корпоративной Jitsi-инфраструктуры.
Локальный запуск после установки зависимостей:
```bash
cd frontend
npm install
npm run dev
```
Проверка сборки:
```bash
cd frontend
npm run lint
npm run format:check
npm run build
```
## Единая проверка этапа
Если доступен `make`:
```bash
make check
```
Те же проверки вручную:
```bash
cd backend
.venv\Scripts\python -m ruff check .
.venv\Scripts\python -m ruff format --check .
.venv\Scripts\python -m mypy
.venv\Scripts\python -m pytest
cd ..\frontend
npm run lint
npm run format:check
npm run build
```
## Правило разработки
Работа идёт строго по этапам. Следующий этап не начинается, пока текущий не запускается, не проверен и не соответствует критериям приёмки.