Тактичный робот: умеет слушать и не перебивает

  • Tutorial
Распознавание речи (далее – ASR, Automatic Speech Recognition) используется при создании ботов и/или IVR, а также для автоматизированных опросов. Voximplant использует ASR, предоставляемый «корпорацией добра» – гугловское распознавание работает быстро и с высокой точностью, но… Как всегда, есть один нюанс. Человек может делать паузы даже в коротких предложениях, при этом нам нужна гарантия, что ASR не воспримет паузу как окончание ответа. Если ASR думает, что человек закончил говорить, то после «ответа» сценарий может включить синтез голоса со следующим вопросом – в это же самое время человек продолжит говорить и получит плохой пользовательский опыт: бот/IVR перебивает человека. Сегодня мы расскажем, как с этим бороться, чтобы ваши пользователи не огорчались от общения с железными помощниками.


Концепция


Цель – задать вопрос и выслушать человека, не перебивая и ожидая окончания его ответа. ASR у нас представлен отдельным модулем, где есть событие ASR.Result – оно триггерится, когда человек закончил говорить. Специфика работы ASR от Google в том, что ASR.Result с распознанным текстом вернётся, как только человек сделает хотя бы небольшую паузу и google решит, что сказанная фраза распознана и закончена.

Чтобы дать человеку возможность делать паузы, можно использовать событие ASR.InterimResult. В нём ASR в процессе распознавания возвращает весь «сырой» текст, корректируя и меняя его в зависимости от контекста – и так вплоть до срабатывания ASR.Result. Таким образом, событие ASR.InterimResult является показателем того, что человек в данный момент что-либо говорит. Мы будем ориентироваться лишь на него и смотреть, как долго оно не приходит. А промежуточные распознанные тексты, полученные из ASR.Result – складывать.

В общем виде это будет выглядеть так:

asr.addEventListener(ASREvents.InterimResult, e => {
  clearTimeout(timer)
  timer = setTimeout(stop, 3000)
})
asr.addEventListener(ASREvents.Result, e => {
  answer += " " + e.text
})
function stop(){
//...
}

Раскрываем суть. Таймеры


Для правильной работы с паузами можно создать специальный объект:

timeouts = {
  silence: null,
  pause: null,
  duration: null
}

После заданного вопроса человек зачастую задумывается на несколько секунд. Таймер на тишину в самом начале лучше выставлять 6-8 секунд, ID таймера мы сохраним в параметр timeouts.silence.

Паузы в середине ответа оптимальны в 3-4 секунды, чтобы человек мог задуматься, но не мучился в ожидании, когда договорил. Это параметр timeouts.pause.

Общий таймер на весь ответ – timeouts.duration – пригодится, если мы не хотим, чтобы человек разговаривал слишком долго. Также это оградит нас от случаев, когда человек находится в шумном помещении с фоновыми голосами, которые будут приниматься нами за речь клиента. А также от случаев, когда мы попали на другого робота, который разговаривает с нашим роботом по кругу.

Итого, в начале сценария мы подключаем модуль ASR, объявляем переменные и создаем объект timeouts:

require(Modules.ASR)

let call,
    asr,
    speech = ""

timeouts = {
  silence: null,
  pause: null,
  duration: null
}

Входящий звонок


Когда в сценарий поступает входящий звонок, срабатывает событие AppEvents.CallAlerting. Создадим обработчик для этого события: ответить на звонок, поприветствовать клиента, запустить распознавание после приветствия. А ещё давайте позволим человеку перебить робота с середины задаваемого вопроса (подробности – чуть дальше).

обработчик AppEvents.CallAlerting
VoxEngine.addEventListener(AppEvents.CallAlerting, e => {

  call = e.call

  // отвечаем на входящий звонок. При соединении отловим событие Connected 
  call.answer()
  call.addEventListener(CallEvents.Connected, e => {
    call.say("Здравствуйте, вы оформляли заказ на нашем сайте. Расскажите, пожалуйста, как вы оцениваете удобство работы с нашим сервисом?", Language.RU_RUSSIAN_FEMALE)

    // начнём слушать через 4 секунды и дадим возможность с этого момента перебивать робота
    setTimeout(startASR, 4000)
    // включим все остальные таймеры по окончанию вопроса
    call.addEventListener(CallEvents.PlaybackFinished, startSilenceAndDurationTimeouts)
  });
  call.addEventListener(CallEvents.Disconnected, e => {
    VoxEngine.terminate()
  })
})

Видно, что вызываются функции startASR и startSilenceAndDurationTimeouts – разберем, что это и зачем.

Распознавание и таймауты


Распознавание реализовано в функции startASR. Она создает инстанс ASR и направляет голос человека в этот инстанс, также она содержит обработчики для событий ASREvents.InterimResult и ASREvents.Result. Как мы говорили выше, здесь мы трактуем ASR.InterimResult как признак, что человек говорит. Обработчик этого события очищает ранее созданные таймауты, задает новое значение для timeouts.pause и, наконец, останавливает синтезированный голос (вот так человек сможет перебивать бота). Обработчик ASREvents.Result просто конкатенирует все итоговые ответы в переменной speech. Конкретно в этом сценарии speech никак не используется, но при желании ее можно передать на ваш бэкенд, к примеру.

startASR
function startASR() {
  asr = VoxEngine.createASR({
    lang: ASRLanguage.RUSSIAN_RU,
    interimResults: true
  })

  asr.addEventListener(ASREvents.InterimResult, e => {
    clearTimeout(timeouts.pause)
    clearTimeout(timeouts.silence)

    timeouts.pause = setTimeout(speechAnalysis, 3000)
    call.stopPlayback()
  })

  asr.addEventListener(ASREvents.Result, e => {
    // складываем распознаваемые ответы
    speech += " " + e.text
  })

  // направляем поток в ASR
  call.sendMediaTo(asr)
}

Функция startSilenceAndDurationTimeouts… Записывает значения соответствующих таймеров:

function startSilenceAndDurationTimeouts() {
  timeouts.silence = setTimeout(speechAnalysis, 8000)
  timeouts.duration = setTimeout(speechAnalysis, 30000)
}

И еще немного функций


speechAnalysis останавливает распознавание и анализирует текст из speech (который получен из ASREvents.Result). Если текста нет, то мы повторяем вопрос; если текст есть, то вежливо прощаемся и кладем трубку.

speechAnalysis
function speechAnalysis() {
  // останавливаем модуль ASR
  stopASR()
  const cleanText = speech.trim().toLowerCase()

  if (!cleanText.length) {
    // если переменная с нулевой длиной, то это значит что сработал таймер тишины,
    // т.е. человек вообще ничего не ответил, и мы можем, например, повторить вопрос абоненту
    handleSilence()
  } else {
    call.say(
      "Большое спасибо за отзыв! До свидания!",
      Language.RU_RUSSIAN_FEMALE
    )
    call.addEventListener(CallEvents.PlaybackFinished, () => {
      call.removeEventListener(CallEvents.PlaybackFinished)
      call.hangup()
    })
  }
}

За повторение вопроса отвечает handleSilence:

function handleSilence() {
  call.say("Извините, вас не слышно. Расскажите, пожалуйста, как вы оцениваете удобство работы с нашим сервисом?", Language.RU_RUSSIAN_FEMALE)

  // начнём слушать через 3 секунды и дадим возможность с этого момента перебивать робота
  setTimeout(startASR, 3000)
  call.addEventListener(CallEvents.PlaybackFinished, startSilenceAndDurationTimeouts)
}

Наконец, функция-хелпер для останова ASR:

function stopASR() {
  asr.stop()
  call.removeEventListener(CallEvents.PlaybackFinished)
  clearTimeout(timeouts.duration)
}

Все вместе


листинг сценария

Итоговый сценарий показывает, как можно «облагородить» прямолинейного робота, добавив ему немного такта и внимания. Наверняка этот способ – не единственно возможный, поэтому если у вас возникнут мысли, как еще можно элегантно допилить привычное взаимодействие бота и человека – делитесь в комментах. Тем же, кто хочет чего-то более продвинутого и вдруг не читал наш туториал про DialogFlow – рекомендуем ознакомиться.

Voximplant

176,00

Облачная платформа голосовой и видеотелефонии

Поделиться публикацией
Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое