commit 7e80baee9480d1589f40c0d2e191aa0ca6b871ae Author: Prokhor Date: Fri Jul 17 08:15:07 2026 +0000 Загрузить файлы в «/» diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..54f3a48 --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# Application +APP_ENV=development +APP_SECRET_KEY=change-me +CORS_ORIGINS=http://localhost:5173 +ACCESS_TOKEN_TTL_SECONDS=28800 +INITIAL_ADMIN_EMAIL=admin@example.local +INITIAL_ADMIN_PASSWORD=change-me-admin +INITIAL_ADMIN_NAME=Администратор + +# Database +POSTGRES_DB=meeting_assistant +POSTGRES_USER=meeting +POSTGRES_PASSWORD=password +POSTGRES_PORT=5432 +DATABASE_URL=postgresql+psycopg://meeting:password@postgres:5432/meeting_assistant +DATABASE_CONNECT_TIMEOUT_SECONDS=3 + +# Queue +REDIS_URL=redis://redis:6379/0 + +# Jitsi +JITSI_DOMAIN=meet.company.local +VITE_JITSI_BASE_URL=https://meet.company.local +VITE_JITSI_REQUIRE_JWT=false +JITSI_APP_ID=meeting-assistant +JITSI_APP_SECRET=change-me +JITSI_TOKEN_TTL_SECONDS=7200 + +# Internal processing +PROCESSING_TOKEN=change-me +PROCESSING_JOB_TIMEOUT_SECONDS=10800 +FFMPEG_BINARY=ffmpeg +FFMPEG_TIMEOUT_SECONDS=1800 +RECORDINGS_ROOT=/data/recordings +RECORDING_VIDEO_RETENTION_DAYS=30 +RECORDING_CLEANUP_INTERVAL_SECONDS=3600 +AUDIO_STORAGE_DIR=/data/audio +AUDIO_ROOT=/data/audio +TRANSCRIPTS_STORAGE_DIR=/data/transcripts +TRANSCRIPTS_ROOT=/data/transcripts +REPORTS_ROOT=/data/reports + +# Whisper +WHISPER_ENGINE=faster-whisper +WHISPER_MODEL=large-v3 +WHISPER_DEVICE=cpu +WHISPER_COMPUTE_TYPE=int8 +WHISPER_LANGUAGE=ru +WHISPER_DOWNLOAD_ROOT=/models/faster-whisper +WHISPER_TIMEOUT_SECONDS=7200 + +# Ollama / Gemma +OLLAMA_BASE_URL=http://ollama:11434 +OLLAMA_MODEL=gemma3:12b +OLLAMA_TIMEOUT_SECONDS=1800 +OLLAMA_CHUNK_MAX_CHARS=12000 + +# Email workflow +N8N_WEBHOOK_URL=http://n8n:5678/webhook/meeting-report +N8N_WEBHOOK_SECRET=change-me +N8N_TIMEOUT_SECONDS=30 +AUTO_SEND_EMAILS=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..beb97f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.pyre/ +.tox/ +.nox/ +.coverage +.coverage.* +coverage.xml +htmlcov/ +pytest-tmp*/ +.pytest_tmp/ +*.egg-info/ +pip-wheel-metadata/ +.venv/ +venv/ +env/ +ENV/ + +# Node / frontend +node_modules/ +dist/ +build/ +.vite/ +.cache/ +coverage/ +*.tsbuildinfo +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Logs and local process files +*.log +*.pid +*.pid.lock +.dev-server*.log + +# Environment and secrets +.env +.env.* +.envrc +*.local +!.env.example +!**/.env.example + +# Runtime data +data/recordings/* +data/audio/* +data/transcripts/* +data/reports/* +!data/recordings/.gitkeep +!data/audio/.gitkeep +!data/transcripts/.gitkeep +!data/reports/.gitkeep + +# Infrastructure runtime mounts and generated state +infrastructure/jitsi/config/ +infrastructure/jitsi/transcripts/ +infrastructure/jibri/config/ +infrastructure/jibri/recordings/ +infrastructure/worker-mac/logs/ +infrastructure/ollama-mac/logs/ +docker-compose.override.yml +infrastructure/**/docker-compose.override.yml + +# Local release/archive artifacts +meeting-assistant-code-update-*.zip +meeting-assistant-code-update-*.zip.sha256 + +# OS/editor +.DS_Store +Thumbs.db +desktop.ini +.idea/ +.vscode/ diff --git a/DEPLOY_CODE_UPDATE_FROM_ARCHIVE.md b/DEPLOY_CODE_UPDATE_FROM_ARCHIVE.md new file mode 100644 index 0000000..a221307 --- /dev/null +++ b/DEPLOY_CODE_UPDATE_FROM_ARCHIVE.md @@ -0,0 +1,102 @@ +# Обновление кода на Ubuntu из архива + +Этот архив предназначен для обновления кода проекта `meeting-assistant` поверх уже настроенного сервера. + +Архив не должен заменять серверные секреты, данные и runtime-артефакты: + +- `.env` +- `data/` +- `backend/.venv/` +- `frontend/node_modules/` +- `frontend/dist/` +- `.git/` +- модели Whisper/Gemma/Ollama +- данные n8n +- записи, аудио, транскрипты и отчёты + +## 1. Перенести архив на сервер + +С Fedora или другой машины: + +```bash +scp meeting-assistant-code-update-*.zip deploy@UBUNTU_SERVER:/tmp/ +``` + +Если пользователь на сервере другой, замени `deploy`. + +## 2. Зайти на сервер + +```bash +ssh deploy@UBUNTU_SERVER +``` + +## 3. Сделать резервную копию текущего кода + +```bash +sudo cp -a /opt/ai_agent_bobik /opt/ai_agent_bobik.backup.$(date +%Y%m%d-%H%M%S) +``` + +Если проект лежит не в `/opt/ai_agent_bobik`, используй свой путь. + +## 4. Распаковать архив во временную папку + +```bash +rm -rf /tmp/meeting-assistant-update +mkdir -p /tmp/meeting-assistant-update +unzip /tmp/meeting-assistant-code-update-*.zip -d /tmp/meeting-assistant-update +``` + +## 5. Синхронизировать код поверх проекта + +```bash +rsync -avh --delete \ + --exclude='.env' \ + --exclude='.git/' \ + --exclude='data/' \ + --exclude='backend/.venv/' \ + --exclude='frontend/node_modules/' \ + --exclude='frontend/dist/' \ + --exclude='*.log' \ + /tmp/meeting-assistant-update/ \ + /opt/ai_agent_bobik/ +``` + +Важно: слэш в конце `/tmp/meeting-assistant-update/` означает “скопировать содержимое папки”. + +## 6. Проверить, что серверные настройки остались + +```bash +test -f /opt/ai_agent_bobik/.env && echo ".env на месте" +ls -la /opt/ai_agent_bobik/data +``` + +## 7. Применить миграции и пересобрать только сервисы приложения + +```bash +cd /opt/ai_agent_bobik +docker compose -f infrastructure/docker-compose.yml run --rm migrator +docker compose -f infrastructure/docker-compose.yml up -d --build backend frontend worker recording-cleanup +``` + +Если n8n, Ollama/Gemma, Whisper-модели и FFmpeg уже установлены отдельно, их не переустанавливай. Для n8n на другой машине достаточно, чтобы в серверном `.env` был правильный `N8N_WEBHOOK_URL`. + +## 8. Проверить после обновления + +```bash +curl http://localhost:8000/health +curl http://localhost:8000/health/db +docker compose -f infrastructure/docker-compose.yml ps +``` + +## 9. Если нужно откатиться + +```bash +sudo systemctl stop docker +sudo rm -rf /opt/ai_agent_bobik +sudo cp -a /opt/ai_agent_bobik.backup.YYYYMMDD-HHMMSS /opt/ai_agent_bobik +sudo systemctl start docker +cd /opt/ai_agent_bobik +docker compose -f infrastructure/docker-compose.yml up -d +``` + +Подставь фактическое имя backup-папки. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..486824f --- /dev/null +++ b/Makefile @@ -0,0 +1,56 @@ +.PHONY: help backend-test backend-lint backend-format-check backend-typecheck backend-alembic-heads backend-seed frontend-lint frontend-format-check frontend-build check + +help: + @echo "meeting-assistant" + @echo "Stage 0 scaffold is in progress." + @echo "" + @echo "Backend:" + @echo " cd backend && .venv\\Scripts\\uvicorn app.main:app --reload" + @echo " make backend-test" + @echo " make backend-lint" + @echo " make backend-format-check" + @echo " make backend-typecheck" + @echo " make backend-alembic-heads" + @echo " make backend-seed" + @echo "" + @echo "Frontend:" + @echo " cd frontend && npm install" + @echo " cd frontend && npm run dev" + @echo " make frontend-build" + @echo " make frontend-lint" + @echo " make frontend-format-check" + @echo "" + @echo "All checks:" + @echo " make check" + @echo "" + @echo "Docker:" + @echo " docker compose -f infrastructure/docker-compose.yml up --build" + +backend-test: + cd backend && .venv\Scripts\python.exe -m pytest + +backend-lint: + cd backend && .venv\Scripts\python.exe -m ruff check . + +backend-format-check: + cd backend && .venv\Scripts\python.exe -m ruff format --check . + +backend-typecheck: + cd backend && .venv\Scripts\python.exe -m mypy + +backend-alembic-heads: + cd backend && .venv\Scripts\python.exe -m alembic heads + +backend-seed: + cd backend && .venv\Scripts\python.exe -m app.seed + +frontend-lint: + cd frontend && npm run lint + +frontend-format-check: + cd frontend && npm run format:check + +frontend-build: + cd frontend && npm run build + +check: backend-lint backend-format-check backend-typecheck backend-test backend-alembic-heads frontend-lint frontend-format-check frontend-build diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2420de --- /dev/null +++ b/README.md @@ -0,0 +1,256 @@ +# 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 +``` + +## Правило разработки + +Работа идёт строго по этапам. Следующий этап не начинается, пока текущий не запускается, не проверен и не соответствует критериям приёмки.