Словарь замен расширен под нишу: категории прав, документы, госорганы, термины обучения и оплаты, частые ошибки на плохом звуке. В docs - инструкция для агента Talkscore: что выключить на своей стороне, как ставить задачи и принимать вебхук, и главное - почему полю speaker доверять нельзя и как восстанавливать роли через LLM. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
Интеграция 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 процентов речи, то есть модель просто не различает голоса.
Что с этим делать:
- Смотрите на
stats.speakers_reliable. Еслиfalse(а это обычный случай), разметку по говорящим нужно строить заново - по смыслу реплик. - Роли определяет LLM. Готовый промпт - в разделе ниже.
- Границы реплик и тайм-коды достоверны - их даёт детектор речи, и он работает хорошо. Опираться можно на них, а не на номер говорящего.
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в браузере, открывается с разрешённых адресов.