Files
talkscore-asr/docs/ИНТЕГРАЦИЯ-TALKSCORE.md
T
Vladimir BryzgalovandClaude Opus 5 a1f420d61c Словарь автошколы и документ по интеграции с Talkscore
Словарь замен расширен под нишу: категории прав, документы, госорганы,
термины обучения и оплаты, частые ошибки на плохом звуке.

В docs - инструкция для агента Talkscore: что выключить на своей стороне,
как ставить задачи и принимать вебхук, и главное - почему полю speaker
доверять нельзя и как восстанавливать роли через LLM.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 01:15:11 +05:00

11 KiB
Raw Blame History

Интеграция Talkscore с локальным ASR-сервисом

Документ для агента, который будет дорабатывать Talkscore. Описывает, что изменить на стороне Talkscore, чтобы использовать локальный сервис распознавания talkscore-asr вместо облачного ASR.

Что это за сервис

Локальный сервис на Windows-машине: принимает аудиофайл, возвращает расшифровку с разделением по говорящим. Работает офлайн, аудио наружу не уходит.

  • Распознавание: GigaAM v3 (SaluteDevices), русский язык, с пунктуацией
  • Разделение говорящих: sherpa-onnx с моделями pyannote и NeMo TitaNet
  • Скорость: около 60 минут записи за 2,5 минуты

Адрес и токен спросите у владельца: сервис закрыт списком разрешённых адресов, поэтому сервер Talkscore нужно в этот список внести.

Главное, что нужно изменить в Talkscore

1. Отключить предобработку аудио

Сейчас в Talkscore включена нормализация громкости (RMS до -20 dBFS) перед отправкой в ASR. Её нужно выключить - сервис делает нормализацию сам, причём более подходящую (dynaudnorm), и лишний проход только тратит ресурсы сервера.

Замер на восьми разговорах: нормализация dynaudnorm даёт долю второго участника 24,3 % против 3,5 % у нормализации RMS. Двойная обработка не ломает результат, но и не улучшает его.

Что оставить включённым: ничего из блока предобработки не требуется. VAD и шумоподавление сервису не нужны - у него свой VAD внутри диаризации.

2. Заменить вызов ASR

Было: отправка в облачный ASR и ожидание ответа. Стало: постановка задачи и получение результата вебхуком.

import requests

ASR_URL = "http://АДРЕС:8756"
ASR_TOKEN = "токен из config.toml сервиса"

def send_to_asr(file_path: str, call_id: str) -> str:
    """Ставит запись в очередь распознавания. Возвращает id задачи."""
    with open(file_path, "rb") as f:
        response = requests.post(
            f"{ASR_URL}/v1/jobs",
            headers={"Authorization": f"Bearer {ASR_TOKEN}"},
            params={
                "speakers": 2,
                "webhook": f"https://talkscore.ru/api/asr-callback?call_id={call_id}",
            },
            files={"file": f},
            timeout=300,
        )
    response.raise_for_status()
    return response.json()["job_id"]

3. Принять результат вебхуком

Сервис сам постучится, когда задача готова. Подпись тела лежит в заголовке X-Talkscore-Signature, секрет задаётся в настройках сервиса.

import hashlib
import hmac

WEBHOOK_SECRET = "тот же секрет, что в config.toml сервиса"

def asr_callback(request):
    signature = request.headers.get("X-Talkscore-Signature", "")
    expected = hmac.new(WEBHOOK_SECRET.encode(), request.body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, signature):
        return 403

    payload = request.json()
    if payload["status"] != "done":
        # обработать ошибку: payload["error"]
        return 200

    save_transcript(payload)
    return 200

Если вебхук не дошёл (три попытки: сразу, через 30 секунд, через 5 минут), результат остаётся в сервисе и его можно забрать опросом: GET /v1/jobs/{job_id} с тем же заголовком авторизации.

Формат результата

{
  "job_id": "fd47ba135eae429cbf37fb6ec1d8c34c",
  "status": "done",
  "filename": "call.mp3",
  "duration_sec": 1003.0,
  "turns": [
    {
      "speaker": 1,
      "start": 7.2,
      "end": 9.4,
      "text": "Ну, давайте послушаю вас ещё.",
      "acoustics": {"loudness_db": -18.4, "hf_ratio": 0.208, "centroid_hz": 1706}
    }
  ],
  "stats": {
    "speakers": 2,
    "speech_sec": 557.0,
    "silence_sec": 446.1,
    "turns_count": 137,
    "separation_quality": 0.27,
    "speakers_reliable": false,
    "by_speaker": [
      {"speaker": 1, "speech_sec": 236.8, "share_pct": 42.5},
      {"speaker": 2, "speech_sec": 320.2, "share_pct": 57.5}
    ]
  },
  "timing": {"diarization_sec": 29.1, "asr_sec": 9.7, "realtime_factor": 25.9}
}

Самое важное: полю speaker доверять нельзя

Записи делаются одним микрофоном на столе, оба участника в одной акустике. Проверка на восьми разговорах: разделение по голосам сработало только на одном из восьми. На остальных один участник получал от 91 до 99 процентов речи, то есть модель просто не различает голоса.

Что с этим делать:

  1. Смотрите на stats.speakers_reliable. Если false (а это обычный случай), разметку по говорящим нужно строить заново - по смыслу реплик.
  2. Роли определяет LLM. Готовый промпт - в разделе ниже.
  3. Границы реплик и тайм-коды достоверны - их даёт детектор речи, и он работает хорошо. Опираться можно на них, а не на номер говорящего.
  4. acoustics - подсказка. У того, кто ближе к микрофону, громкость и доля высоких частот стабильно выше. Это дополнительный сигнал для LLM.

Промпт для восстановления ролей

Расставлять роли поверх реплик диаризации бесполезно: когда она провалилась, в одной реплике оказываются и вопрос, и ответ. Поэтому текст режется на предложения, а модель отвечает одной буквой на предложение - так выход получается в десятки раз короче, чем при возврате текста.

Подготовка: склейте текст всех реплик, разрежьте по границам предложений (GigaAM ставит точки и вопросительные знаки), пронумеруйте.

Ты разбираешь запись разговора в автошколе: менеджер и клиент.
Микрофон стоял на столе, автоматическое разделение по голосам не сработало,
поэтому реплики склеены - в одной строке может быть и вопрос одного,
и ответ другого.

Ниже пронумерованные предложения по порядку. Определи для каждого, кто его
произнёс.

Как отличить:
- МЕНЕДЖЕР (M): рассказывает об условиях, ценах, документах, расписании;
  отвечает на вопросы; предлагает записаться; говорит «у нас», «мы»,
  «вам нужно принести», называет суммы и сроки.
- КЛИЕНТ (C): спрашивает про стоимость, сроки, расписание; рассказывает
  о себе и своей ситуации; сомневается; сравнивает с другими автошколами;
  соглашается или уточняет; говорит «а если», «мне нужно», «я слышал».

Подсказки:
- Вопрос и ответ на него принадлежат разным людям.
- Короткие «да», «ага», «понятно», «конечно» обычно принадлежат слушающему,
  то есть тому, кто НЕ произносил предыдущее длинное объяснение.
- Менеджер говорит больше, но не непрерывно: клиент постоянно вставляет
  короткие реплики.

Ответь строкой ровно из {count} символов, только M и C, без пробелов и переносов,
по одному символу на предложение в том же порядке.

Предложения:
{sentences}

После ответа склейте соседние предложения с одинаковой ролью обратно в реплики, сохранив тайм-коды первого предложения каждой группы.

Словарь замен терминов

В сервисе лежит файл replacements.txt со словарём под автошколу: категории прав, документы, госорганы, термины обучения и оплаты, частые ошибки распознавания. Формат что слышно = как надо, замена по целым словам, регистр не важен.

Файл перечитывается перед каждой задачей, перезапуск не нужен. Если в расшифровках попадаются устойчивые ошибки - дописывайте строки туда.

Что ещё стоит знать

  • Форматы: принимается всё, что читает ffmpeg - mp3, wav, m4a, ogg, opus, wma.
  • Размер: по умолчанию до 500 МБ, настраивается.
  • Очередь: задачи обрабатываются по очереди, статус и место в очереди видны в GET /v1/jobs/{id}.
  • Диагностика: GET /health работает без токена, показывает версию, очередь и то, каким сервис видит адрес обратившегося.
  • Журнал: GET /v1/logs?level=ERROR - последние записи, чтобы разбирать сбои не заходя на машину.
  • Описание методов: http://АДРЕС:8756/docs в браузере, открывается с разрешённых адресов.