# Интеграция 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 и ожидание ответа. Стало: постановка задачи и получение результата вебхуком. ```python 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`, секрет задаётся в настройках сервиса. ```python 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}` с тем же заголовком авторизации. ## Формат результата ```json { "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` в браузере, открывается с разрешённых адресов.