CRM: создать видеовстречу и получить расшифровку
Пользователь нажимает в CRM «Создать видеовстречу», участники общаются в
VideoGrace, транскрибер распознает речь на лету. После завершения его работы
CRM получает готовый текст через transcription.ready и сохраняет в БД.
Скачивать запись и повторно распознавать разговор не нужно.
Схема и подготовка
sequenceDiagram
participant UI as Пользователь CRM
participant CRM as Backend CRM
participant VG as VideoGrace
participant T as CAN Transcriber
participant DB as БД CRM
UI->>CRM: Создать видеовстречу
CRM->>VG: PUT /integration/rooms/{external_id}
VG-->>CRM: tag и персональные ссылки входа
CRM->>DB: Сохранить связь встречи с комнатой
CRM-->>UI: Войти во встречу
UI->>VG: Подключиться
Note over VG,T: Администратор/автоматизация отдельно запускает transcriber.start
VG->>T: CAN job с ID созвона
loop Во время разговора
VG-->>T: Аудио участников по RTP
T->>T: Распознавать речь
T->>VG: Фрагменты в чат
end
Note over VG,T: Конец звонка или остановка транскрибера
T->>T: Завершить последнюю фразу
T->>VG: completed + итоговый transcript artifact
VG->>CRM: Подписанный webhook transcription.ready
CRM->>DB: Сохранить текст и event.id в транзакции
CRM-->>VG: HTTP 204
UI->>CRM: Открыть карточку встречи
CRM-->>UI: Расшифровка
Администратор VideoGrace подготавливает:
- HTTPS-адрес, доступные медиапорты, Integration API и серверный JWT secret.
- Отдельный партнерский service account, например
crm, со scopeintegration:rooms. - CAN Transcriber с отдельным сервисным аккаунтом и способ запуска
transcriber.startпри нужном созвоне. Создание комнаты само по себе транскрибер не запускает, в партнерском VideoRoom кнопка управления им скрыта. - Исходящий HTTPS-доступ от сервера к webhook CRM.
Нужны обновленные сервер и worker с поддержкой финального artifact. Проверка:
GET /api/v1/integration/server_info должен сообщать feature
webhooks.transcription_ready. Для собственной модели ASR достаточно
реализовать контракт CAN-воркера.
Партнер получает адрес, имя своего service account и client_secret.
JWT secret сервера, CAN token и секрет транскрибера партнеру/браузеру не выдаются.
Настройка аккаунтов: Integration API.
1. Получить токен на backend CRM
В примерах используются curl и jq; секреты хранятся в окружении backend.
export VG_BASE_URL='https://video.example.com'
export VG_SERVICE_ACCOUNT='crm'
# VG_CLIENT_SECRET задается через менеджер секретов.
VG_ACCESS_TOKEN=$(curl --fail-with-body -sS \
"$VG_BASE_URL/api/v1/integration/auth/token" \
-H 'Content-Type: application/json' \
--data "$(jq -n \
--arg account "$VG_SERVICE_ACCOUNT" \
--arg secret "$VG_CLIENT_SECRET" \
'{service_account: $account, client_secret: $secret}')" \
| jq -er '.access_token')
Кэшируйте токен на срок expires_in, затем получайте новый через тот же endpoint.
2. Создать встречу
Стабильный ID встречи CRM используйте как external_id. Для повторного
запроса тот же ID, для новой встречи новый ID.
curl --fail-with-body -sS -X PUT \
"$VG_BASE_URL/api/v1/integration/rooms/crm_meeting_987" \
-H "Authorization: Bearer $VG_ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
--data '{
"title": "Обсуждение предложения по сделке 542",
"description": "Встреча из CRM",
"teacher": {"external_id": "employee:42", "display_name": "Иван Петров"},
"student": {"external_id": "contact:108", "display_name": "Мария Иванова"}
}'
Основные поля ответа:
{
"room_id": 123,
"external_id": "crm_meeting_987",
"tag": "lesson-a8f3k2",
"join_url": "https://video.example.com/conferences/lesson-a8f3k2",
"teacher_join_url": "https://video.example.com/j/<manager-token>",
"student_join_url": "https://video.example.com/j/<customer-token>"
}
teacher и student являются техническими именами ролей: менеджер получает
ссылку модератора, клиент ссылку участника. Пример рассчитан на двоих.
Персональную ссылку нельзя раздавать нескольким людям: она авторизует
конкретный аккаунт. Передавайте неизменный ID каждого человека из CRM.
Сохраните external_id, room_id, tag в БД. PUT идемпотентен, но не
обновляет существующую комнату; для изменений есть PATCH по тому же адресу.
Ссылку VideoGrace открывайте в отдельной вкладке, SDK/iframe не обязательны.
3. Зарегистрировать webhook
# VG_WEBHOOK_SECRET: отдельный случайный секрет длиной 16-256 символов.
curl --fail-with-body -sS -X PUT \
"$VG_BASE_URL/api/v1/integration/webhook" \
-H "Authorization: Bearer $VG_ACCESS_TOKEN" \
-H 'Content-Type: application/json' \
--data "$(jq -n --arg secret "$VG_WEBHOOK_SECRET" '{
url: "https://crm.example.com/webhooks/videograce",
secret: $secret,
events: ["transcription.ready"],
enabled: true
}')"
Webhook применяется к комнатам этого партнерского аккаунта. Если нужны и
записи, укажите оба события: ["transcription.ready", "recording.ready"].
Для скачивания записей потребуется дополнительный scope blob;
для текста, уже содержащегося в webhook, он не нужен.
4. Принять текст в CRM
Сокращенный пример события:
{
"id": "wh_example_delivery",
"type": "transcription.ready",
"created_at": "2026-09-13T10:30:05Z",
"data": {
"room": {"external_id": "crm_meeting_987", "tag": "lesson-a8f3k2"},
"transcription": {
"id": "transcriber-job-001",
"job_id": "transcriber-job-001",
"conference_tag": "lesson-a8f3k2",
"conference_session_id": 1842,
"status": "completed",
"language": "ru",
"started_at_ms": 1789293600000,
"ended_at_ms": 1789295400000,
"reason": "empty_conference",
"text": "Договорились о пилоте.",
"segments": [
{"seq": 1, "at": 1789293605000, "speaker": {"client_id": 101, "device_id": 1000, "name": "Иван"}, "text": "Договорились о пилоте.", "partial": false}
]
}
}
}
Сначала проверьте подпись по исходным байтам, до JSON-разбора. Пример функции Node.js / TypeScript:
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyVideoGrace(
rawBody: Buffer, timestamp: string, signature: string, secret: string,
): boolean {
if (!/^\d+$/.test(timestamp) || !/^sha256=[a-f0-9]{64}$/i.test(signature)) return false;
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false;
const expected = createHmac("sha256", secret)
.update(timestamp + ".").update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(signature.slice(7), "hex"));
}
Значения берутся из X-VideoGrace-Timestamp и X-VideoGrace-Signature.
Размер HTTP body ограничьте на приемнике с учетом длинных разговоров:
payload содержит весь текст одного запуска, не только последнее предложение.
После проверки подписи и валидации JSON сохраните результат в транзакции. Ниже псевдокод адаптера CRM, не готовый SDK VideoGrace:
async function saveTranscription(event: TranscriptionReadyEvent) {
const { room, transcription: t } = event.data;
await crm.transaction(async (tx) => {
// UNIQUE(integrationId, eventId); повтор webhook безопасен.
if (!await tx.insertWebhookOnce(integrationId, event.id)) return;
const meeting = await tx.findMeetingByExternalId(room.external_id);
if (!meeting) throw new Error("Unknown CRM meeting");
await tx.upsertTranscript({
integrationId,
meetingId: meeting.id,
conferenceSessionId: t.conference_session_id,
transcriberJobId: t.job_id,
startedAtMs: t.started_at_ms,
endedAtMs: t.ended_at_ms,
text: t.text,
segments: t.segments,
});
});
}
Возвращайте 2xx только после commit или надежного сохранения события в
очереди CRM. При ошибке БД возвращайте 5xx, чтобы сервер повторил доставку.
Для саммаризации ставьте отдельную фоновую задачу, не держите HTTP-запрос.
Полные заголовки, подпись и расписание повторов:
Integration API.
5. Правильно связать части разговора
transcription.ready означает завершение одного запуска транскрибера, а не
закрытие комнаты. При выключении и повторном включении будет несколько
событий с одним conference_session_id и разными job_id.
Ключ хранения части: сервер/интеграция + conference_session_id + job_id.
Общую расшифровку собирайте из этих частей по времени. Не затирайте результат
только по тегу комнаты: в ней может проходить несколько разных созвонов.
segments[].at является временем публикации фрагмента, не точным таймкодом
аудиофайла. Пустой текст тоже допустим, если речь не распознана.
Постоянная очередь VideoGrace защищает уже принятый сервером итог от недоступности CRM и перезапуска сервера. Авария worker до отправки итога или отсутствие запущенного транскрибера не создают событие готовности. Отдельного API архива транскриптов пока нет: CRM должна надежно сохранять входящий текст.
Проверка пилота
- Создать комнату дважды с одинаковым
external_id, убедиться в идемпотентности. - Войти двумя участниками и запустить CAN Transcriber. Убедиться в живых фрагментах.
- Завершить разговор, проверить
transcription.readyи текст в карточке CRM. - Остановить транскрибер посреди фразы: последняя фраза должна войти в итог.
- Включить его повторно в том же звонке: другой
job_id, тот же ID созвона. - Повторить webhook: в БД нет дубля. Вернуть
503: доставка повторяется. - Повторно созвониться в той же комнате: новый ID созвона, отдельный текст.
Участники должны знать о расшифровке разговора. Запись можно подключить
независимо через Recorder и recording.ready;
она не является источником для описанной живой транскрипции.