Skip to content

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, со scope integration: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 должна надежно сохранять входящий текст.

Проверка пилота

  1. Создать комнату дважды с одинаковым external_id, убедиться в идемпотентности.
  2. Войти двумя участниками и запустить CAN Transcriber. Убедиться в живых фрагментах.
  3. Завершить разговор, проверить transcription.ready и текст в карточке CRM.
  4. Остановить транскрибер посреди фразы: последняя фраза должна войти в итог.
  5. Включить его повторно в том же звонке: другой job_id, тот же ID созвона.
  6. Повторить webhook: в БД нет дубля. Вернуть 503: доставка повторяется.
  7. Повторно созвониться в той же комнате: новый ID созвона, отдельный текст.

Участники должны знать о расшифровке разговора. Запись можно подключить независимо через Recorder и recording.ready; она не является источником для описанной живой транскрипции.