В рамках проекта отдела качества мы анализируем работу продающих менеджеров по SLA (Service Level Agreement — соглашение об уровне обслуживания, что описывает параметры предоставляемой услуги). И решили протестировать несколько инструментов speech-to-text для транскрибации звонков. Среди тройки финалистов оказался и яндексовский SpeechSense, о нем и пойдет речь в статье.
Перед запуском
Pre Requisites: macOS, Python 3.10
Документация у SpeechSense Getting Started, мягко говоря, дырявая, и мне потребовалось также продираться через рекомендации саппорта, логи GitHub официального репозитория и бесполезные советы ChatGPT в течение нескольких часов.
Прежде чем последовать докам, создадим виртуальное окружение с той версией Python, на которой, вероятно, и пилят SpeechSense. Настоятельно рекомендую не игнорировать этот шаг:
python3.10 -m venv speechsense_env
Если создавать окружение оператором python / python3 (без указания конкретной версии), будет создано окружение на глобальной версии языкового пакета (у меня 3.11.5, это обронит весь пайплайн). Зайдем в окружение:
source speechsense_env/bin/activate
Чтобы заполнить некоторые обязательные аргументы скрипта подгрузки, нам потребуется создать облачное пространство в Yandex Cloud, подвести к нему SpeechSense как решение и сгенерировать проект с подключением. С этими шагами документация справляется благополучно.
Если вкратце, сервисный аккаунт создается так:
API-ключ тоже несложно получить. Идем по адресу console.yandex.cloud (предварительно авторизовавшись) и генерируем токен:
Клонируем репозиторий:
git clone https://github.com/yandex-cloud/cloudapi
Устанавливаем необходимые библиотеки:
pip3 install grpcio-tools googleapis-common-protos==1.5.10 protobuf==3.20.0
Затем создадим рабочую субдиректорию:
mkdir upload_data
Клонируем гугловскую библиотеку api-common-protos:
git clone https://github.com/googleapis/api-common-protos.git
Благодаря этим пунктам удастся избежать ошибки:
ImportError: cannot import name 'talk_service_pb2' from 'yandex.cloud.speechsense.v1' (unknown location)
Донастроим процедуру передачи файла на облако. Намеренно привожу пример со своими путями до директорий, чтобы не осталось вопросов, как выглядит этот шаг в июле 2024. Обратите внимание на две строки снизу: они отличаются от документации, и только с ними импорт talk_service_pb2 будет успешным:
python3 -m grpc_tools.protoc -I . -I /Users/elenakapatsa/Repositories/cloudapi/api-common-protos
--python_out=/Users/elenakapatsa/Repositories/cloudapi/upload_data
--grpc_python_out=/Users/elenakapatsa/Repositories/cloudapi/upload_data
yandex/cloud/speechsense/v1/.proto
yandex/cloud/speechsense/v1/analysis/.proto &&
На деле же мы задаем настройки gRPC (Google Remote Procedure Call) — способ вызвать тот или иной метод на сервере так, будто это происходит локально, на машине для разработки. Почему при замене путей до .proto на вышеуказанные все начинает работать, не понятно. В документации по-другому.
В случае SpeechSense, мы вызываем методы:
загрузки звонка в формате .wav в свое пространство
транскрибации звонка
анализа звонка по SLA
Если установка библиотек прошла без проблем, команда выше исполнится успешно и мы сможем перейти к самому скрипту подгрузки дорожки в облако. Заходим в рабочую папку:
cd upload_data
И теперь верстаю скрипт launch_me.sh с путем до файла:
python3 upload_grpc.py
--audio-path /Users/elenakapatsa/Repositories/cloudapi/wav_records/843401-b73a483915c1459fa1fdb07c11274a6b.wav
--connection-id "<Идентификатор подключения>"
--key "<Токен>"
Если токен сервисного аккаунта мы уже получили выше, то вот откуда брать connection-id (после создания облака, проекта и подключения):
В сам скрипт upload_grpc.py
закладываем код согласно документации:
import argparse
import json
from typing import Dict
import grpc
import datetime
from yandex.cloud.speechsense.v1 import talk_service_pb2
from yandex.cloud.speechsense.v1 import talk_service_pb2_grpc
from yandex.cloud.speechsense.v1 import audio_pb2
# Для аутентификации с IAM-токеном замените параметр api_key на iam_token
def upload_talk(connection_id: int, metadata: Dict[str, str], api_key: str, audio_bytes: bytes):
credentials = grpc.ssl_channel_credentials()
channel = grpc.secure_channel('api.talk-analytics.yandexcloud.net:443', credentials)
talk_service_stub = talk_service_pb2_grpc.TalkServiceStub(channel)
# Формирование запроса к API
request = talk_service_pb2.UploadTalkRequest(
metadata=talk_service_pb2.TalkMetadata(
connection_id=str(connection_id),
fields=metadata
),
# Формат аудио — WAV
audio=audio_pb2.AudioRequest(
audio_metadata=audio_pb2.AudioMetadata(
container_audio=audio_pb2.ContainerAudio(
container_audio_type=audio_pb2.ContainerAudio.ContainerAudioType.CONTAINER_AUDIO_TYPE_WAV
)
),
audio_data=audio_pb2.AudioChunk(data=audio_bytes)
)
)
# Тип аутентификации — API-ключ
response = talk_service_stub.Upload(request, metadata=(
('authorization', f'Api-Key {api_key}'),
# Для аутентификации с IAM-токеном передавайте заголовок
# ('authorization', f'Bearer {iam_token}'),
))
# Вывести идентификатор диалога
print(f'Dialog ID: {response.talk_id}')
if name == 'main':
parser = argparse.ArgumentParser()
parser.add_argument('--key', required=True, help='API key or IAM token', type=str)
parser.add_argument('--connection-id', required=True, help='Connection ID', type=str)
parser.add_argument('--audio-path', required=True, help='Audio file path', type=str)
parser.add_argument('--meta-path', required=False, help='JSON with the dialog metadata', type=str, default=None)
args = parser.parse_args()
# Значения по умолчанию, если метаданные не указаны
if args.meta_path is None:
now = datetime.datetime.now().isoformat()
metadata = {
'operator_name': 'Operator',
'operator_id': '1111', # Телфин отдает ID в панели статистики
'client_name': 'Client',
'client_id': '2222', # Сюда подставляют ID клиента из нашей CRM
'date': str(now),
'date_from': '2023-09-13T17:30:00.000',
'date_to': '2023-09-13T17:31:00.000',
'direction_outgoing': 'true',
}
else:
with open(args.meta_path, 'r') as fp:
metadata = json.load(fp)
with open(args.audio_path, 'rb') as fp:
audio_bytes = fp.read()
upload_talk(args.connection_id, metadata, args.key, audio_bytes)
На этом страдания завершены. После успешной вгрузки дорожки система отдает ID диалога в терминал:
(speechsense_env) elenakapatsa@kea-macbook-pro upload_data % bash launch_me.sh
Dialog ID: aud6lvar29df8crff2pt
Поделитесь в комментариях, как бы вы доверстали launch_me.sh, чтобы передавать циклом все дорожки в папке. Там еще дополнительные аргументы подавать предстоит (ID оператора, ID клиента).
Настал черед пройти в веб-интерфейс SpeechSense и посмотреть, что же такого нейронка нараспознавала и как она справляется с типичными грехами speech-to-text-инструментов, как то:
некорректно распознаваемые имена собственные и редкие слова (наш Инфостарт нередко становится «Инфостаром», «Инфобиржей» и даже «СССР»)
некорректная / отсутствуюшая пунктуация, усложняющая службе качества «скорочтение» транскрибации
неверно распознанные слова при дефектах речи (например, менеджер прикрыл рукой микрофон) и проч.
В моем проекте SpeechSense сразу отображаются транскрибированные звонки, и само устройство консоли выгодно отличает продукт от других опробованных (T-Bank VoiceKit, Таймлист 1С):
А вот и Killer Feature: YandexGPT сам оценивает менеджера по базовым параметрам (вежливость, вовлеченность, раздраженность, успешность и проч.). Если LLM что-то не учла, ей можно отправить фидбэк в догонку, чтобы в следующую итерацию результат улучшился. И дополнительные рубли не тратятся! Такое мы используем.
Чего не хватает стандартному промпту YaGPT, доделывается тегами. Теперь можно искать какие угодно события в массивах звонков:
Разговор удобно разбит на реплики, каждую из которых можно воспроизвести нажатием одной кнопки.
Развитая система аналитики, позволяющая строить отчеты буквально по любому параметру:
Изыскивать трудовые ресурсы на интеграцию не нужно: с провайдерами вроде Телфина Яндекс связывается напрямую.
Инструмента, который бы задвинул в угол конкурентов, пока не существует. Есть у продукта Яндекс и недостатки:
Подавать звонки лучше в .mp3, чтобы сохранить двухканальность. Иначе не везде голос оператора будет отделен от голоса покупателя.
Плохо восстановленная пунктуация. Тот же YandexGPT «внахлест» точно улучшит ситуацию.
Очень напрашивается дообучение на месте, чтобы поправить неверно распознанное имя собственное сразу, прямо в тексте реплики. Наши инженеры отдела качества смиренно дорабатывают транскрибацию вручную и саму оценку по SLA даже в 2024 году. Высокой степени автоматизации все так же не получается.
Кстати, на 10 звонков (длятся в среднем по 6,5 минут) ушло 55 рублей. Тариф для малых оборотов — 1,02 руб. за минуту. Обещают допилить API к концу 2024
Несмотря ни на что, очень хочется похвалить создателей. Несмотря на бессмысленные сложности с развертыванием, SpeechSense оставляет впечатление продуманного, оттестированного и превосходяшего конкурентов решения. «Важно быть счастливым в любом состоянии», даже если простой запуск демки занял слишком много времени.