Skip to content

Transcriber

Services/Transcriber - Python CAN worker для live-транскрибации конференций. Он подключается к /can, принимает transcriber.start, входит в конференцию как сервисный receiver, подписывается на аудио renderer streams и отправляет обновления расшифровки в чат конференции.

Для локальной работы на Apple Silicon поддерживается mlx_whisper. Штатная Linux/Windows x64 поставка использует faster-whisper на CPU/int8 (small по умолчанию), отдельный дежурный worker и сервисный аккаунт transcriber.

Установка вместе с сервером

Для CAN и транскрибации нужен действующий ключ сервера (триальный или полный). В бесплатном режиме без ключа 4/12 worker не получает задания; сервер возвращает license_required. Установка Python и наличие сервисного токена этого ограничения не снимают. Правила остановки заданий описаны в документации CAN.

В Linux-архив включены исходники worker, requirements-runtime.txt и unit vg-transcriber.service. Windows x64 регистрирует задачу VideoGrace Standby Transcriber. Python, сторонние пакеты и модели не увеличивают основной архив: runtime подготавливается при установке, модель загружается при первом задании. Старый системный Python в Linux не заменяется. Подробности и offline-настройка: установка Linux.

Сервер создаёт учётную запись с правами conference:join, media:render, chat:write при запуске, если уже есть владелец, либо при первом задании. Существующий аккаунт не перезаписывается и не включается принудительно. Внешний CAN-worker может использовать эту же сервисную идентичность, но ему нужен отдельный node_id. Если одновременно подключены штатный и внешний worker, задание получит один из доступных узлов: для выделенного внешнего ASR отключите локальный vg-transcriber или адресуйте задание нужному узлу.

Старт дежурного worker не включает расшифровку всех встреч. Нужен отдельный transcriber.start; также это не автоматическая настройка CRM webhook.

Lifecycle

sequenceDiagram
    participant Core
    participant CAN as Transcriber CAN worker
    participant WS as CommandLoop
    participant RTP as RTP audio
    participant MLX as mlx_whisper
    participant Chat

    CAN->>Core: /can hello(role=transcriber)
    Core-->>CAN: job transcriber.start(auth.access_token, conference_tag)
    CAN->>WS: connect_request(access_token)
    CAN->>WS: connect_to_conference_request(tag)
    WS-->>CAN: device_connect microphone renderer
    CAN->>RTP: receiver_ssrc adjust packet
    RTP-->>CAN: plain RTP Opus 48 kHz
    CAN->>MLX: 3-5 sec PCM window
    MLX-->>CAN: text
    CAN->>Chat: delivery_messages transcript payload

Production job

{
  "job_type": "transcriber.start",
  "target_role": "transcriber",
  "required_capability": "speech_to_text",
  "payload": {
    "server_url": "https://video.example.com",
    "conference_tag": "teamsink",
    "source": {
      "type": "conference_audio"
    },
    "model": "mlx-community/whisper-large-v3-turbo",
    "language": "ru",
    "window_sec": 4,
    "max_windows": 0
  }
}

Если payload.auth отсутствует, core пытается выпустить bearer JWT из service_accounts. По умолчанию используется service account с именем transcriber; альтернативное имя можно передать как payload.service_account.

После инъекции auth worker получает:

{
  "auth": {
    "type": "bearer",
    "access_token": "eyJ..."
  }
}

Идентичность, видимость и права

Для production нужен отдельный пользователь transcriber, созданный через панель администрирования. Нельзя использовать аккаунт основателя конференции: совпадение client_id смешивает обычное и сервисное подключения и не позволяет проверить реальное поведение отдельного бота.

В этой схеме четыре независимых уровня:

Уровень Механизм Назначение
Идентичность запись в clients Имя и стабильный client_id транскрибера.
Авторизация сервиса service_accounts и scopes conference:join, media:render, chat:write; для файлов также storage:write Выпуск короткого JWT без хранения пароля на worker.
Тип подключения MemberType::Service Вход в конференцию как service receiver и исключение из обычных плиток участников web-клиента.
Ограничение медиа MemberGrants::ReadOnly Запрет публикации микрофона, камеры и демонстрации; не управляет видимостью и отправкой текста.

Отдельного grant Hidden в протоколе нет и он не требуется. После проверки service JWT сервер назначает подключению MemberType::Service. Web-клиент фильтрует участников этого типа из списка обычных плиток, но продолжает использовать их состояние для индикации работающего транскрибера или рекордера.

Service-пользователя не требуется добавлять постоянным участником каждой конференции: MemberType::Service проходит отдельную серверную проверку доступа. Worker отправляет connect_to_conference_request с has_camera=false, has_microphone=false и has_demonstration=false, поэтому не публикует свои устройства. ReadOnly можно применять как дополнительную защиту от случайной публикации, но он не является способом скрыть бота.

Текст расшифровки отправляется через delivery_messages по той же control WS, которая вошла в конференцию. Scope chat:write дает необходимый control-доступ, а сервер принимает сообщение от активного service-участника. ReadOnly этому не мешает. Transcript-сообщения сохраняются и доставляются в чат, но намеренно не создают мобильные push-уведомления, чтобы live-обновления расшифровки не засыпали пользователей уведомлениями.

Если worker прикладывает к итоговому сообщению MP3 или другой файл, service account дополнительно нужен scope storage:write. Worker должен создать upload session, загрузить и завершить blob через HTTPS Storage API, а затем включить в текст сообщения ссылку /api/storage/blobs/<blob_id>. Один CAN artifact event или локальный путь worker'а вложение к сообщению не создаёт.

flowchart LR
    User["clients: transcriber"] --> Account["service_accounts"]
    Account -->|"short JWT"| Service["MemberType::Service"]
    Service -->|"media:render"| Audio["remote microphone renderers"]
    Service -->|"chat:write / delivery_messages"| Chat["conference transcript"]
    Service -. "filtered from participant tiles" .-> UI["VideoRoom UI"]
    ReadOnly["ReadOnly: optional media guard"] -.-> Service

RTP assumptions

Текущий low-level media path повторяет путь recorder receiver:

  • каждый renderer socket принимает один аудиопоток;
  • поток приходит как plain RTP Opus 48 kHz;
  • шифрование и mux на этом участке не используются;
  • receiver_ssrc открывает RTP path через adjust packet;
  • author_ssrc связывает текст с конкретным remote microphone device;
  • транскрибация идет речевыми сегментами: обычный сегмент закрывается после паузы, а VG_TRANSCRIBER_MAX_SEGMENT_SEC работает как мягкий лимит и ждет ближайшую короткую паузу, чтобы не резать длинную фразу на полуслове;
  • VG_TRANSCRIBER_HARD_MAX_SEGMENT_SEC остается аварийным верхним пределом для непрерывной речи без пауз.

Chat payload

Транскрибер отправляет структурированное сообщение:

{
  "type": "transcript",
  "version": 2,
  "job_id": "transcriber-...",
  "conference_tag": "teamsink",
  "conference_session_id": 1842,
  "partial": false,
  "update_seq": 1,
  "updated_at": 1786200000000,
  "segments": [
    {
      "seq": 1,
      "at": 1786200000000,
      "speaker": {"client_id": 228800001, "device_id": 1071, "name": "Anton"},
      "text": "...",
      "partial": false
    }
  ]
}

conference_session_id задается сервером при создании CAN job и связывает расшифровку с конкретным сеансом комнаты. Для аналитики нельзя использовать один conference_tag: одна и та же комната может запускаться многократно.

Остановка и повторный запуск транскрибера во время того же созвона меняют job_id, но не conference_session_id. Саммаризатор объединяет результаты всех нужных запусков по ID сеанса и сохраняет его в своём результате, даже если обработка завершилась после звонка. Не подменяйте его текущим активным ID комнаты. Полный контракт и границы реализации описаны в статистике созвонов.

GUID сообщения формируется стабильно:

transcript_{job_id}_{speaker_key}

Это позволяет клиенту обновлять один live transcript block, а не засыпать чат отдельными сообщениями каждые несколько секунд.

Итог живой транскрипции и webhook CRM

Распознавание выполняется на лету. При штатной остановке (job_cancel, transcriber.stop, завершение конференции) worker дожидается текущего распознавания и последнего буфера речи. Он отправляет итог через CAN, даже если command WSS конференции уже закрыт. Временный разрыв command WSS не сбрасывает накопленные сегменты; сам по себе он не отправляет итог.

Контракт для собственного ASR-воркера:

{
  "type": "job_event",
  "job_id": "transcriber-job-001",
  "status": "completed",
  "data": {
    "schema": "videograce.transcription.artifact.v1",
    "transcription": {
      "job_id": "transcriber-job-001",
      "conference_tag": "lesson-a8f3k2",
      "conference_session_id": 1842,
      "language": "ru",
      "started_at_ms": 1789293600000,
      "ended_at_ms": 1789295400000,
      "reason": "external_stop",
      "segments": [
        {"seq": 1, "at": 1789293605000, "speaker": {"client_id": 101, "device_id": 1000, "name": "Иван"}, "text": "Договорились о пилоте.", "partial": false}
      ]
    }
  }
}

ID задания, conference_tag и conference_session_id берутся из исходной transcriber.start job. Не запрашивайте текущий сеанс комнаты при завершении: там уже может идти другой звонок. speaker и text обязательны у каждого сегмента. segments: [] является корректным результатом без распознанной речи. Не отправляйте completed до финального artifact: готовый текст передается в том же событии. job_context worker не передает, это внутреннее поле CAN.

Для партнерской комнаты сервер формирует transcription.ready и кладет его в постоянную очередь подписавшегося партнера. Запись, S3 и повторный ASR не нужны для доставки текста. Несколько запусков транскрибера дают несколько итогов одного conference_session_id, которые объединяет CRM.

Обновление только сервера недостаточно: сторонний worker тоже должен реализовать этот финальный CAN artifact. Создание комнаты через Integration API не включает транскрибер автоматически; оркестрация transcriber.start остается отдельной серверной настройкой интеграции. Аварийное завершение процесса или потеря CAN до отправки результата не гарантируют его доставку.

Примеры подписки, payload, проверка подписи и повторы: Integration API, CRM: встреча и расшифровка.

Локальная проверка

Services/Transcriber/run_transcriber.sh --help

Wrapper создает .venv-transcriber, ставит зависимости, выставляет PYTHONPATH и на macOS автоматически добавляет /opt/homebrew/lib в DYLD_LIBRARY_PATH. Для protocol-only проверок без установки Whisper backend можно указать VG_TRANSCRIBER_INSTALL_DEPS=core; обычный запуск ставит полный backend из Services/Transcriber/requirements.txt, если он еще не установлен.

CAN worker:

VG_CAN_URL=wss://video.example.com/can \
VG_CAN_SERVICE_TOKEN=... \
VG_SERVER_URL=https://video.example.com \
Services/Transcriber/run_transcriber.sh

Protocol-only self-test без MLX:

Services/Transcriber/run_transcriber.sh --self-test \
  --conference-tag teamsink \
  --text "Тестовая расшифровка"

Инварианты

  • Transcriber не публикует свои microphone/camera devices.
  • Transcriber подключается к конференции как сервисный receiver и слушает remote microphone devices.
  • Пароль сервисного пользователя не должен попадать в CAN job; production path - bearer JWT от service_accounts.
  • CAN переносит только control plane, media идет напрямую по RTP.