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.