В рамках проекта отдела качества мы анализируем работу продающих менеджеров по 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

Я копировала небольшую подборку звонков прямо в репозиторий cloudapi:
Я копировала небольшую подборку звонков прямо в репозиторий cloudapi:

И теперь верстаю скрипт 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С):

Успешно распознанные менеджер и покупатель
Успешно распознанные менеджер и покупатель
Неудачно распознанные менеджер и покупатель. Подаем в двухканальном .mp3!
Неудачно распознанные менеджер и покупатель. Подаем в двухканальном .mp3!
  • А вот и Killer Feature: YandexGPT сам оценивает менеджера по базовым параметрам (вежливость, вовлеченность, раздраженность, успешность и проч.). Если LLM что-то не учла, ей можно отправить фидбэк в догонку, чтобы в следующую итерацию результат улучшился. И дополнительные рубли не тратятся! Такое мы используем.

Можно фидбэкнуть работу YandexGPT
Можно фидбэкнуть работу YandexGPT
  • Чего не хватает стандартному промпту YaGPT, доделывается тегами. Теперь можно искать какие угодно события в массивах звонков:

Теги демо-проекта турфирмы
Теги демо-проекта турфирмы
  • Разговор удобно разбит на реплики, каждую из которых можно воспроизвести нажатием одной кнопки.

  • Развитая система аналитики, позволяющая строить отчеты буквально по любому параметру:

Посмотрим, кто из операторов был вежлив
Посмотрим, кто из операторов был вежлив
  • Изыскивать трудовые ресурсы на интеграцию не нужно: с провайдерами вроде Телфина Яндекс связывается напрямую.

Инструмента, который бы задвинул в угол конкурентов, пока не существует. Есть у продукта Яндекс и недостатки:

  • Подавать звонки лучше в .mp3, чтобы сохранить двухканальность. Иначе не везде голос оператора будет отделен от голоса покупателя.

  • Плохо восстановленная пунктуация. Тот же YandexGPT «внахлест» точно улучшит ситуацию.

Очень напрашивается дообучение на месте, чтобы поправить неверно распознанное имя собственное сразу, прямо в тексте реплики. Наши инженеры отдела качества смиренно дорабатывают транскрибацию вручную и саму оценку по SLA даже в 2024 году. Высокой степени автоматизации все так же не получается.

Кстати, на 10 звонков (длятся в среднем по 6,5 минут) ушло 55 рублей. Тариф для малых оборотов — 1,02 руб. за минуту. Обещают допилить API к концу 2024

Несмотря ни на что, очень хочется похвалить создателей. Несмотря на бессмысленные сложности с развертыванием, SpeechSense оставляет впечатление продуманного, оттестированного и превосходяшего конкурентов решения. «Важно быть счастливым в любом состоянии», даже если простой запуск демки занял слишком много времени.

Комментарии (0)