README: голосовой режим, запись и транскрипция, env, ресурсы, служебные эндпоинты

This commit is contained in:
crm-calls agent
2026-10-15 07:19:23 +00:00
parent 65427235b2
commit 49fba12653

View File

@@ -66,12 +66,65 @@
`JWT_ACCEPTED_AUDIENCES=jitsi`, `JWT_APP_SECRET` (env Coolify), `XMPP_MUC_MODULES=token_affiliation` (модератор з пропуску),
`JICOFO_ENABLE_AUTH=0` і `ENABLE_AUTO_OWNER=false` (інакше jicofo робить модератором кожного з токеном, гостей теж),
`DISABLE_DEEP_LINKING=true`, `CSP_HEADER: frame-ancestors https://crm-calls.volcanic.tes.gd` (вбудовувати Jitsi може лише модуль).
Пам'ять Java: `VIDEOBRIDGE_MAX_MEMORY=512m`, `JICOFO_MAX_MEMORY=256m` (типово було 3072m — більше за весь сервер).
**Лише голос (рішення власника 2026-10-15).** `jitsi/custom-config.js` дописується в `config.js` сервера (власний образ
`jitsi/web.Dockerfile`), тож діє і в браузері, і в мобільному додатку Jitsi Meet (він бере `config.js` з нашого сервера):
`startAudioOnly`, без кнопок камери й демонстрації екрана, без гарячих клавіш (`V`/`D`). У пропуску JWT вимкнено
`screen-sharing`, `recording`, `livestreaming` (`context.features`). Камеру в мобільному додатку повністю заборонити
з сервера не можна: додаток стартує в режимі «лише звук», але кнопку камери у своєму меню може показати.
### Заголовки модуля
`Content-Security-Policy: frame-ancestors <CRM_ORIGINS>; script-src 'self' https://meet.volcanic.tes.gd; frame-src https://meet.volcanic.tes.gd; …`
(без `X-Frame-Options`), `Referrer-Policy: no-referrer`, `Permissions-Policy` дозволяє камеру й мікрофон собі й Jitsi.
## Запис і транскрипція
1. **Початок дзвінка.** Модуль раз на 5 с питає prosody (`mod_muc_size`, `GET http://jitsi-prosody:5280/room`), хто в кімнатах,
де видавали пропуски за останні 13 год. Коли в кімнаті ≥ `RECORD_MIN_PARTICIPANTS` (2) учасників, модуль запускає
Jibri через його HTTP API (`POST http://jitsi-jibri:2222/jibri/api/v1.0/startService`, запис у файл, прихований користувач
`recorder`). Учасникам нічого натискати не треба, працює і для гостей, і для мобільного додатка.
2. **Кінець дзвінка.** Кімната порожня 20 с → `stopService`. Тривалість дзвінка рахується за присутністю учасників.
3. **Jibri finalize** (`jitsi/finalize.sh`, власний образ `jitsi/jibri.Dockerfile`): лише звук (моно, 16 кГц, mp3 32 кбіт/с),
**відео видаляється одразу**, звук ріжеться на частини по `CALLS_CHUNK_SECONDS` (1200 с ≈ 4,7 МБ, ліміт Whisper 25 МБ),
`meta.json` (тривалість, частини, статистика тиші й гучності) і маркер `ready`.
4. **Обробка в модулі** (кроки зберігаються, повтор продовжує з місця зупинки):
- дзвінок коротший за `MIN_CALL_SECONDS` → файли видаляються, сторінки немає;
- немає `OPENAI_API_KEY` або `CRM_USER_EMAIL`/`CRM_USER_PASSWORD` → запис чекає на диску (статус `waiting`), назовні нічого не йде;
- Whisper (`OPENAI_TRANSCRIBE_MODEL`, `verbose_json`, сегменти; мова — автоматично) частина за частиною, тайміги
кожної частини зсуваються на тривалість попередніх; результат — `transcript.json`;
- вхід користувача CRM у Supabase (password grant) → аудіо в Storage: `<RECORDINGS_BUCKET>/<RECORDINGS_PREFIX>YYYY/MM/<випадкові 24 hex>/dzvinok-YYYY-MM-DD-HHMM.mp3`,
посилання — публічне (`/storage/v1/object/public/…`, як вкладення CRM у бакеті `files`);
- сторінка у вікі воркспейсу дзвінка, папка `WIKI_CALLS_FOLDER` у корені (створюється, якщо немає), від того самого користувача CRM:
«Дзвінок YYYY-MM-DD HH:MM» (Europe/Warsaw), дата й час, тривалість, учасники (гості з позначкою «гість»), посилання на аудіо,
транскрипція рядками `[гг:хх:сс] текст`;
- **лише після** завантаження аудіо і створення сторінки локальні файли видаляються.
- Помилка будь-якого кроку → файли лишаються, повтор через 1, 2, 5, 10, 30, потім щогодини; текст помилки — у журналі модуля
(Coolify → Logs) і в `GET /api/admin/calls`. Немає доступу користувача CRM до воркспейсу у вікі → помилка
«… не має доступу до воркспейсу …», обходу немає.
5. **Ліміт дзвінків** `MAX_CONCURRENT_CALLS` (1): дзвінок зайнятий, поки в кімнаті є учасники (плюс бронь кімнати на 2 хв після
видачі пропуску). У іншу кімнату пропуск не видається: модуль і вікно дзвінка показують «Зараз уже йде інший дзвінок,
спробуйте пізніше». У дзвінок, що вже йде, входити можна. Jibri — один екземпляр (не менше за ліміт).
Модуль і Jitsi — різні застосунки Coolify; модуль бачить `jitsi-prosody` і `jitsi-jibri` у спільній docker-мережі
`crm-calls-bridge` (її створює застосунок Jitsi). Файли записів — спільний каталог хоста `/data/crm-calls/recordings`
(модуль при старті віддає його uid 1000 — користувачу `s6` у Jibri). HTTP API Jibri слухає `0.0.0.0:2222` лише всередині docker-мереж, назовні порт не відкрито.
### Ресурси (VPS 2 vCPU / 3,8 ГБ + своп 4 ГБ, тест 2026-10-15, звонок 8 хв, 2–3 учасники, запис)
| | Простій | Під час запису |
|---|---|---|
| Jibri | 190 МБ (поки Chrome «прогрівається» — до 760 МБ) | 730–780 МБ, CPU у середньому 24 % (старт запису — до 100 % на кілька секунд) |
| JVB | 165 МБ | 255 МБ, 12 % |
| jicofo / prosody / web / модуль | 145 / 32 / 15 / 30 МБ | 152 / 38 / 17 / 40–60 МБ |
| Хост: доступно пам'яті | ≈ 1,8–2,0 ГБ | 1,33–1,62 ГБ (у середньому 1,57) |
| Хост: своп | 150–230 МБ | 245–255 МБ, не росте |
Якість запису (тест: два учасники з тонами 440 і 660 Гц): жодних провалів ≥ 0,3 с, крім задуманих (3,5 хв без мікрофонів і
15 с після виходу), обидва голоси однаково чутні, втрат пакетів 0, джитер 2–3 мс. Запис не обривається, коли всі мовчать або
вимкнули мікрофони (таймаути Jibri «немає медіа» / «всі без мікрофонів» підняті до 4 год).
## Змінні оточення (Coolify)
Секрети — **лише** в env Coolify, у репозиторії їх немає.
@@ -90,11 +143,26 @@
| `JITSI_TOKEN_TTL_MINUTES` | строк пропуску | `15` |
| `CALLS_SESSION_HOURS` | скільки живе сесія вікна дзвінка / гостя | `12` |
| `CALLS_PUBLIC_URL` | | `https://crm-calls.volcanic.tes.gd` |
| `OPENAI_API_KEY` | ключ OpenAI для Whisper; без нього записи чекають | **задає власник**, секрет |
| `OPENAI_TRANSCRIBE_MODEL` | модель; потрібні сегменти з таймінгами (`verbose_json`) | `whisper-1` |
| `CRM_USER_EMAIL`, `CRM_USER_PASSWORD` | користувач CRM, від якого завантаження аудіо і сторінки; має бути учасником воркспейсів | **задає власник**, пароль — секрет |
| `RECORDINGS_BUCKET` | бакет Supabase Storage | `files` |
| `RECORDINGS_PREFIX` | папка в бакеті | `calls/` |
| `WIKI_URL` | вікі CRM | `https://wiki-crm.tes.gd` |
| `WIKI_CALLS_FOLDER` | папка у вікі | `Дзвінки` |
| `MIN_CALL_SECONDS` | коротші дзвінки не обробляються | `60` |
| `MAX_CONCURRENT_CALLS` | одночасних дзвінків (Jibri — один) | `1` |
| `RECORD_MIN_PARTICIPANTS` | з якої кількості учасників починається дзвінок і запис | `2` |
| `RECORDING_ENABLED` | `false` — вимкнути запис (ліміт дзвінків лишається) | `true` |
| `JIBRI_RECORDER_PASSWORD` | пароль прихованого користувача recorder, **такий самий, як у Jitsi** | секрет |
| `ADMIN_TOKEN` | службові `/api/admin/*`; порожньо — вимкнені | секрет |
Для бойової CRM: `CRM_ORIGINS="https://test.units.tvorymo.com https://units.tvorymo.com"` (і `CRM_ASSETS_ORIGIN`, якщо стилі звідти) → Redeploy.
**Jitsi (`ibugx0ujnub2pbvtfr3euk9f`):** `JWT_APP_SECRET` (секрет, той самий), `JICOFO_AUTH_PASSWORD`, `JVB_AUTH_PASSWORD` (секрети),
`PUBLIC_URL=https://meet.volcanic.tes.gd`, `JVB_PORT=10000`, `JVB_ADVERTISE_IPS=164.90.233.156`, `TZ`, `DEFAULT_LANGUAGE`.
**Jitsi (`ibugx0ujnub2pbvtfr3euk9f`):** `JWT_APP_SECRET` (секрет, той самий), `JICOFO_AUTH_PASSWORD`, `JVB_AUTH_PASSWORD`,
`JIBRI_RECORDER_PASSWORD`, `JIBRI_XMPP_PASSWORD` (секрети), `PUBLIC_URL=https://meet.volcanic.tes.gd`, `JVB_PORT=10000`,
`JVB_ADVERTISE_IPS=164.90.233.156`, `TZ`, `DEFAULT_LANGUAGE`, `VIDEOBRIDGE_MAX_MEMORY` (512m), `JICOFO_MAX_MEMORY` (256m),
`CALLS_AUDIO_BITRATE` (32k), `CALLS_CHUNK_SECONDS` (1200).
Заміна `JWT_APP_SECRET`: однакове нове значення в обох застосунках → Redeploy обох.
@@ -113,9 +181,13 @@
| `POST` | `/api/call/pass` | — | `{ session }` → `{ jwt, exp, room, domain, name, kind }`; `401` — сесія завершилась |
| `GET` | `/api/guest/{token}` | — | `200` — посилання дійсне, `404` — ні |
| `POST` | `/api/guest/{token}/join` | — | `{ name }` → `{ session, kind: "guest", name }` |
| `POST` | `/api/admin/test-link` | `X-Admin-Token` | `{ workspace: "test-…", regenerate? }` → гостьове посилання тестового воркспейсу (не CRM); записи таких дзвінків не публікуються, файли видаляються |
| `GET` | `/api/admin/calls` | `X-Admin-Token` | останні 30 дзвінків: стан обробки (`recording`, `ended`, `waiting`, `error`, `done`, `skipped`), помилка, учасники |
`409` на `/api/join` і `/api/call/pass` — «Зараз уже йде інший дзвінок» (`MAX_CONCURRENT_CALLS`).
Токен CRM: `Authorization: Bearer <access_token>`. Помилки: `{ "error": "…" }`, `401` — токен не прийнято, `403` — немає доступу до воркспейсу.
Дані: SQLite `/data/calls.db` на томі `calls-data` (таблиці `rooms`, `guest_links`, `join_codes`, `call_sessions`; секрети — лише хешами).
Дані: SQLite `/data/calls.db` на томі `calls-data` (таблиці `rooms`, `guest_links`, `join_codes`, `call_sessions`, `pass_log`, `calls`; секрети — лише хешами).
## Відомі обмеження
@@ -125,6 +197,9 @@
- Сесія вікна дзвінка живе 12 годин і не перевіряє повторно членство у воркспейсі: людина, яку прибрали з воркспейсу, може
перепідключитися до кінця сесії. Новий вхід з CRM уже не пройде.
- Гості, які увійшли за старим посиланням, після перегенерації лишаються в дзвінку й можуть перепідключатися до кінця своєї сесії.
- Учасник, який пробув у дзвінку менше ніж ~5 с (період опитування), може не потрапити в список учасників на сторінці.
- Коли в дзвінок заходить третій учасник, Jitsi переходить з P2P на JVB: у цей момент можливі кілька секунд «замаскованого» звуку.
- Передеплой застосунку Jitsi перезапускає всі його контейнери — дзвінки, що йдуть, обриваються.
## Мережа