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

Структура

backend/
frontend/
infrastructure/
data/
  recordings/
  audio/
  transcripts/
  reports/
docs/

PostgreSQL

PostgreSQL теперь является частью проекта. При Docker-развёртывании сервис postgres создаёт пустую базу, migrator применяет Alembic-миграции, затем seeder добавляет начальные справочники до запуска backend.

Backend берёт подключение из DATABASE_URL:

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:

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

Пример создания встречи:

{
  "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 и сохраняет путь к файлу записи:

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; вручную его можно запустить так:

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. После первого запуска нужно загрузить модель:

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-скриптом из переменных:

INITIAL_ADMIN_EMAIL=admin@example.local
INITIAL_ADMIN_PASSWORD=change-me-admin
INITIAL_ADMIN_NAME=Администратор

Локальный запуск после установки зависимостей:

cd backend
python -m venv .venv
.venv\Scripts\pip install -r requirements-dev.txt
.venv\Scripts\uvicorn app.main:app --reload

Проверка:

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:

copy .env.example .env
docker compose -f infrastructure/docker-compose.yml up --build

Проверка:

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.

Создание новой ревизии, когда модели уже описаны:

cd backend
.venv\Scripts\python -m alembic revision --autogenerate -m "describe change"

Ручное применение миграций:

cd backend
.venv\Scripts\python -m alembic upgrade head

Ручное заполнение начальных справочников:

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-инфраструктуры.

Локальный запуск после установки зависимостей:

cd frontend
npm install
npm run dev

Проверка сборки:

cd frontend
npm run lint
npm run format:check
npm run build

Единая проверка этапа

Если доступен make:

make check

Те же проверки вручную:

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

Правило разработки

Работа идёт строго по этапам. Следующий этап не начинается, пока текущий не запускается, не проверен и не соответствует критериям приёмки.

S
Description
No description provided
Readme 32 KiB
Languages
Makefile 100%