# 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\":\"\",\"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 ``` ## Правило разработки Работа идёт строго по этапам. Следующий этап не начинается, пока текущий не запускается, не проверен и не соответствует критериям приёмки.