Asterisk распознавание речи через Google + умный IVR



Доброго времени суток, уважаемые хабра-пользователи.
В одном проекте необходимо было сделать умный IVR на базе IP-АТС Asterisk. Что подразумевается под словом «умный»: при звонке на определенный номер станция просит назвать имя абонента, человек на другом конце провода называет имя и станция связывает его с нужным абонентом.

В моем случае я использовал уже готовую сборку AsteriskNow с уже предустановленным FreePBX, хотя в данном случае это особой роли не играет, т.к. отличия будут только в редактировании dial-плана.


Шаг 1:


Работа с Google

Первое, что необходимо – это каким-то образом распознать речь позвонившего. На хабре было достаточно (раз, два) статей, как это делать с помощью Google Translate. Я же решил взять готовые скрипты, найденные на просторах github: googletts.agi – для того, что бы научить Asterisk говорить и speech-recog.agi – для того, чтобы Asterisk умел распознавать речь.

Файлы googletts.agi и speech-recog.agi закидываем в папку /var/lib/asterisk/agi-bin.
Для успешной работы скриптов необходимо иметь следующие пакеты: Perl, perl-libwww, IO-Socket-SSL, flac, sox, mpg123. Все пакеты у меня успешно скачались и установились с репозиториев (через yum install), за исключением mpg123, его пришлось качать отдельно.

В googletts.agi меняем значение переменной $lang с en на ru, мы ведь хотим, что бы Asterisk заговорил по-русски.
В speech-recog.agi меняем значение переменной $language с en-US на ru-RU для того, чтобы Google возвращал результат на русском языке.
Все, больше в скриптах я не трогал ничего.

Шаг 2:


Написание dial-плана

Как я говорил выше, у меня установлен FreePBX, поэтому все изменения я буду вносить в файл extensions_custom.conf.
Для начала неплохо поприветствовать позвонившего и дать ему комментарий, что делать дальше.

exten => 100,1,Answer()
exten => 100,n,agi(googletts.agi,”Здравствуйте! После звукового сигнала произнесите имя абонента.”,ru)


Дальше с помощью speech-recog.agi, слушаем, что говорит пользователь, записываем, конвертируем, отправляем в Google и получаем от него результаты.

exten => 100,n(record),agi(speech-recog.agi,ru-RU)

Далее с помощью функции GotoIf делаем проверку того, как отработал скрипт.
Скрипт возвращает следующие значения:

status: возвращает статус выполнения. 0 означает успех
utterance: строка, возвращаемая Google
confidence: значение от 0 до 1, показывающее вероятность правильного распознавания

exten => 100,n,GotoIf($[$["${status}" = "0"] & $["${confidence}" > "0.8"]]?if1:retry)

В случае успеха проверки переходим к событию if1, если неудача, переходим к событию retry, в котором попросим пользователя повторить.

exten => 100,n(retry),agi(googletts.agi,”Пожалуйста, повторите”,ru)

Дальше переходим к работе непосредственно с самой строкой, которую мы получили от Google. Необходимо сравнить полученную строку ${utterance} с каким-то шаблоном и решить, что делать дальше. Воспользуемся функцией GotoIf

exten => 100,n(if1),GotoIf($[“${utterance}” = “вася”]?vasya:retry)

Если строка, полученная от Google, совпадает с «вася», переходим к событию vasya, если не совпадает, просим пользователя повторить.

И осталось только позвонить Васе

exten => 100,n(vasya),Dial(SIP/101)

Dial-план полностью:
exten => 100,1,Answer()
exten => 100,n,agi(googletts.agi,”Здравствуйте! После звукового сигнала произнесите имя абонента.”,ru)
exten => 100,n(record),agi(speech-recog.agi,ru-RU)
exten => 100,n,GotoIf($[$["${status}" = «0»] & $["${confidence}" > «0.8»]]?if1:retry)
exten => 100,n(if1),GotoIf($[“${utterance}” = “вася”]?vasya:retry)
exten => 100,n(retry),agi(googletts.agi,”Пожалуйста повторите”,ru)
exten => 100,n(vasya),Dial(SIP/101)


Вариации на тему

  • Для простоты примера я привел всего одно условие сравнения того, что услышали с шаблоном, по факту их может и должно быть больше.
  • Не очень красиво звучат фразы, которые говорит «железная леди» от Google. В рабочем варианте, конечно, неплохо записать фразы «живым» голосом и воспроизводить их при помощи функции Playback.


Тонкости

При подобном виде работы с Google Translate стоит учитывать, что работает он хорошо, но не совершенно и это нужно учитывать при создании тех шаблонов, с которыми будем сравнивать полученный от Google результат.

Вот пример грабель, на которые я наступил:
Меня зовут Кирилл (две «Л» на конце). Google по каким-то только ему известным причинам через раз возвращал либо “кирил”, либо “кирилл”.

Послесловие

Есть подозрение, что сравнение можно было реализовать каким-нибудь более технологичным способом, с радостью выслушаю ваше мнение и предложения в комментариях.
И еще остается открытый вопрос в масштабе: что будет, если абонентов много, сколько займет время на прохождение всех сравнений, если, конечно, их реализовывать предложенным мной способом. Но для небольшой АТС примерно на 20 абонентов данный способ является приемлемым.

Спасибо за внимание.

UPD

Примеры

В качестве примера я использовал чуть-чуть другой dial-план, но суть от этого не меняется.

Dial-план примера:
exten => 8251,1,Answer()
exten => 8251,n,MixMonitor(/var/spool/asterisk/monitor/8251/${CDR(start)}-${DST-NUM}-${ID_CALL}-full.wav)
exten => 8251,n,agi(googletts.agi,«Пожалуйста скажите имя абонента с которым вас соеденить.»,ru)
exten => 8251,n(record),agi(speech-recog.agi,ru-RU)
exten => 8251,n,GotoIf($[$["${status}" = «0»] & $["${confidence}" > «0.5»]]?if1:retry)
exten => 8251,n(if1),GotoIf($["${utterance}" = «александр»]?al:retry)
exten => 8251,n(al),Dial(SIP/8201)
exten => 8251,n(retry),agi(googletts.agi,«Пожалуйста повторите?»,ru)
exten => 8251,n,goto(record)

Ссылка на запись с успешным распознаванием.
Вывод Astesterisk'а:
— Executing [8251@from-internal:1] Answer(«SIP/8211-00000000», "") in new stack
— Executing [8251@from-internal:2] MixMonitor(«SIP/8211-00000000», "/var/spool/asterisk/monitor/8251/2013-04-24 10:28:03---full.wav") in new stack
— Executing [8251@from-internal:3] AGI(«SIP/8211-00000000», «googletts.agi,»Пожалуйста скажите имя абонента с которым вас соеденить.",ru") in new stack
— Launched AGI Script /var/lib/asterisk/agi-bin/googletts.agi
== Begin MixMonitor Recording SIP/8211-00000000
— Playing '/tmp/16ae8d012843179807cfdabd9a34608f' (escape_digits=) (sample_offset 0)
— Playing '/tmp/ef3ccb070117857a8045932052f3fd7b' (escape_digits=) (sample_offset 0)
— <SIP/8211-00000000>AGI Script googletts.agi completed, returning 0
— Executing [8251@from-internal:4] AGI(«SIP/8211-00000000», «speech-recog.agi,ru-RU») in new stack
— Launched AGI Script /var/lib/asterisk/agi-bin/speech-recog.agi
— <SIP/8211-00000000> Playing 'beep.ulaw' (language 'en')
— <SIP/8211-00000000>AGI Script speech-recog.agi completed, returning 0
— Executing [8251@from-internal:5] GotoIf(«SIP/8211-00000000», «1?if1:retry») in new stack
— Goto (from-internal,8251,6)
— Executing [8251@from-internal:6] GotoIf(«SIP/8211-00000000», «1?al:retry») in new stack
— Goto (from-internal,8251,7)
— Executing [8251@from-internal:7] Dial(«SIP/8211-00000000», «SIP/8201») in new stack
== Using SIP RTP TOS bits 184
== Using SIP RTP CoS mark 5
— Called SIP/8201
— SIP/8201-00000001 is ringing
— SIP/8201-00000001 answered SIP/8211-00000000
— Executing [h@from-internal:1] Hangup(«SIP/8211-00000000», "") in new stack
== Spawn extension (from-internal, h, 1) exited non-zero on 'SIP/8211-00000000'
== Spawn extension (from-internal, 8251, 7) exited non-zero on 'SIP/8211-00000000'
== MixMonitor close filestream
== End MixMonitor Recording SIP/8211-00000000

Ссылка на НЕ удачное распознавание.
Вывод Astesterisk'а:
— Executing [8251@from-internal:1] Answer(«SIP/8211-00000002», "") in new stack
— Executing [8251@from-internal:2] MixMonitor(«SIP/8211-00000002», "/var/spool/asterisk/monitor/8251/2013-04-24 10:36:29---full.wav") in new stack
— Executing [8251@from-internal:3] AGI(«SIP/8211-00000002», «googletts.agi,»Пожалуйста скажите имя абонента с которым вас соеденить.",ru") in new stack
— Launched AGI Script /var/lib/asterisk/agi-bin/googletts.agi
== Begin MixMonitor Recording SIP/8211-00000002
— Playing '/tmp/16ae8d012843179807cfdabd9a34608f' (escape_digits=) (sample_offset 0)
— Playing '/tmp/ef3ccb070117857a8045932052f3fd7b' (escape_digits=) (sample_offset 0)
— <SIP/8211-00000002>AGI Script googletts.agi completed, returning 0
— Executing [8251@from-internal:4] AGI(«SIP/8211-00000002», «speech-recog.agi,ru-RU») in new stack
— Launched AGI Script /var/lib/asterisk/agi-bin/speech-recog.agi
— <SIP/8211-00000002> Playing 'beep.ulaw' (language 'en')
— <SIP/8211-00000002>AGI Script speech-recog.agi completed, returning 0
— Executing [8251@from-internal:5] GotoIf(«SIP/8211-00000002», «1?if1:retry») in new stack
— Goto (from-internal,8251,6)
— Executing [8251@from-internal:6] GotoIf(«SIP/8211-00000002», «0?al:retry») in new stack
— Goto (from-internal,8251,8)
— Executing [8251@from-internal:8] AGI(«SIP/8211-00000002», «googletts.agi,»Пожалуста повторите?",ru") in new stack
— Launched AGI Script /var/lib/asterisk/agi-bin/googletts.agi
— Playing '/tmp/0c5de11c17dda57dabeaebe335110036' (escape_digits=) (sample_offset 0)
— <SIP/8211-00000002>AGI Script googletts.agi completed, returning 0
Share post

Similar posts

Comments 11

    0
    Спасибо за решение, пара вопросов по этому поводу:
    — почему не записать стандартные, повторяющиеся фразы в формат mp3 или gsm, вместо того чтобы обращаться каждый раз к гуглу (”Здравствуйте! После звукового сигнала произнесите имя абонента”)? Это реально в продакшине или для примера показано?
    — какова задержка обработки в обоих случаях: googletts.agi и speech-recog.agi
      +1
      По поводу записанных фраз: я в самом топике написал:
      В рабочем варианте, конечно, неплохо записать фразы «живым» голосом и воспроизводить их при помощи функции Playback.

      По поводу задержки: у меня все происходит где-то за 1 секунду, в редких случаях за две.
    • UFO just landed and posted this here
        0
        Встречал такое у одних ребят, продающих сетевое.
        Идея прикольная, но если не находишься в близких деловых отношениях, запомнить ФИО не всегда просто, да и не всегда хочется, и не всегда надо.
        В итоге через несколько звонков устал слушать долгий автоответчик и на зло всем запомнил кто мне нужен. С цифрами было бы проще.
        Но всё равно круто.
          0
          Это — да, но ведь данный механизм можно использовать как угодно. Например можно называть отдел и переводить по совпадению в группу, варианты есть.
            0
            Тоже бабка на двое гадала. Если говорят Бухгалтерия, то будет отправлять на кого-то из 10 бухов, а они могут быть вообще не связаны между собой и сидеть в разных офисах.
            Набор голосом очень круто, прикольно, у меня даже вызвал увеличение интереса к компании. Но если б циферный можно было сделал параллельно, было бы удобно.
              0
              Вся прелесть в том что все ограничивается лишь вашей фантазией. Вы так же можете распознавать цифры которые называет человек и я уже не говорю о том что добавить функцию ввода номера абонента прямо из IVR не составляет большого труда и укладывается в одну строку.
          0
          А можно аудиозапись тестового звонка?
          Например в случае удачного распознавания и в случае неудачного распознавания, если это возможно, конечно. Очень любопытен результат. В других статьях не было.
            0
            Добавил в топик
            0
            Сделал все как описанно но у меня почему то нет звукового сигнала после которого необходимо проговаривать абонента…
              0
              А что вам cli пишет во время звонка?

            Only users with full accounts can post comments. Log in, please.