Pull to refresh

Asterisk + UniMRCP + VoiceNavigator. Синтез и распознавание речи в Asterisk. Часть 3

Reading time13 min
Views5.9K
Часть 1
Часть 2
Часть 4

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

На Хабре нашелся вот этот пост, который когда-то довольно активно обсуждался. Автор предлагает прослушивать прогноз погоды по телефону, используя множество предзаписанных файлов и xml-информеры с сайта Gismeteo. Мне хотелось бы усовершенствовать данное приложение и показать, как синтез и распознавание облегчают жизнь при построении IVR и получении динамической информации.

Приложение будет запрашивать город, погоду в котором хотелось бы узнать, затем спрашивать время (сегодня днем, завтра вечером и т.д.) и сообщать необходимую информацию.

XML-файл с погодой


Файлы c Gismeteo имеют следующий вид:
<?xml version="1.0" encoding="utf-8"?>
<MMWEATHER>
        <REPORT type="frc3">
                <TOWN index="10381" sname="%C1%E5%F0%EB%E8%ED" latitude="52" longitude="13">
                        <FORECAST day="02" month="08" year="2011" hour="20" tod="3" predict="0" weekday="3">
                                <PHENOMENA cloudiness="0" precipitation="10" rpower="0" spower="0"/>
                                <PRESSURE max="760" min="758"/>
                                <TEMPERATURE max="21" min="19"/>
                                <WIND min="2" max="4" direction="1"/>
                                <RELWET max="74" min="72"/>
                                <HEAT min="19" max="21"/>
                        </FORECAST>
                        <FORECAST day="03" month="08" year="2011" hour="02" tod="0" predict="0" weekday="4">
                                <PHENOMENA cloudiness="0" precipitation="10" rpower="0" spower="0"/>
                                <PRESSURE max="761" min="759"/>
                                <TEMPERATURE max="15" min="13"/>
                                <WIND min="1" max="3" direction="1"/>
                                <RELWET max="83" min="81"/>
                                <HEAT min="13" max="15"/>
                        </FORECAST>
                        <FORECAST day="03" month="08" year="2011" hour="08" tod="1" predict="0" weekday="4">
                                <PHENOMENA cloudiness="0" precipitation="10" rpower="0" spower="0"/>
                                <PRESSURE max="761" min="759"/>
                                <TEMPERATURE max="18" min="16"/>
                                <WIND min="2" max="4" direction="2"/>
                                <RELWET max="80" min="78"/>
                                <HEAT min="16" max="18"/>
                        </FORECAST>
                        <FORECAST day="03" month="08" year="2011" hour="14" tod="2" predict="0" weekday="4">
                                <PHENOMENA cloudiness="1" precipitation="10" rpower="0" spower="0"/>
                                <PRESSURE max="760" min="758"/>
                                <TEMPERATURE max="26" min="24"/>
                                <WIND min="2" max="4" direction="2"/>
                                <RELWET max="56" min="54"/>
                                <HEAT min="22" max="24"/>
                        </FORECAST>
                </TOWN>
        </REPORT>
</MMWEATHER>

Каждый день делится на 4 времени суток: ночь, утро, день, вечер.
Файл всегда показывает погоду на 4 периода, начиная с момента, когда он был обновлен. Обновление файлов просиходит 4 раза в сутки: 2.30, 8.30, 14.30, 20.30 по МСК. В показанном выше файле погода на вечер 2 августа, ночь, утро и день 3-го августа. Эту логику мы будем учитывать при обработке файла и в работе приложения.
Мы будем использовать следующие параметры:
weekday — день недели (1 — воскресенье, 2 — понедельник, и т.д.)
tod — время суток, для которого составлен прогноз (0 — ночь 1 — утро, 2 — день, 3 — вечер)
cloudiness — облачность по градациям (0 — ясно, 1- малооблачно, 2 — облачно, 3 — пасмурно)
precipitation — тип осадков (4 — дождь, 5 — ливень, 6,7 – снег, 8 — гроза, 9 — нет данных, 10 — без осадков)
TEMPERATURE — температура воздуха, в градусах Цельсия

Автоматическое получение xml-файлов


Для того чтобы всегда иметь в наличии актуальные xml-файлы создадим скрипт и добавим его в cron. Скрипт будет забирать файлы для нужных нам городов и складывать в указанную папку. Из него же будем брать названия городов.
#!/bin/bash
DIR=/var/www/html/gismeteo/xml
/usr/bin/wget 'http://informer.gismeteo.ru/xml/27612_1.xml' -O $DIR/27612_1.xml         #Москва
/usr/bin/wget 'http://informer.gismeteo.ru/xml/26063_1.xml' -O $DIR/26063_1.xml         #Санкт-Петербург
/usr/bin/wget 'http://informer.gismeteo.ru/xml/22892_1.xml' -O $DIR/22892_1.xml         #Выборг
/usr/bin/wget 'http://informer.gismeteo.ru/xml/29634_1.xml' -O $DIR/29634_1.xml         #Новосибирск
/usr/bin/wget 'http://informer.gismeteo.ru/xml/31960_1.xml' -O $DIR/31960_1.xml         #Владивосток
/usr/bin/wget 'http://informer.gismeteo.ru/xml/26850_1.xml' -O $DIR/26850_1.xml         #Минск
/usr/bin/wget 'http://informer.gismeteo.ru/xml/33345_1.xml' -O $DIR/33345_1.xml         #Киев
/usr/bin/wget 'http://informer.gismeteo.ru/xml/36870_1.xml' -O $DIR/36870_1.xml         #Алматы
/usr/bin/wget 'http://informer.gismeteo.ru/xml/76680_1.xml' -O $DIR/76680_1.xml         #Мехико
/usr/bin/wget 'http://informer.gismeteo.ru/xml/2974_1.xml' -O $DIR/2974_1.xml           #Хельсинки
/usr/bin/wget 'http://informer.gismeteo.ru/xml/10381_1.xml' -O $DIR/10381_1.xml         #Берлин
/usr/bin/wget 'http://informer.gismeteo.ru/xml/48454_1.xml' -O $DIR/48454_1.xml         #Бангкок

Грамматики


Начнем с построения грамматик. Нам понадобится 3 файла.
towns.xml – файл с городами, погоду в которых мы хотим узнавать. В качестве семантического тега используется имя xml-файла с сервера Gismeteo. Можно добавить все города, погода в которых может быть Вам потенциально интересна))
<?xml version="1.0" encoding="utf-8"?>
<grammar xml:lang="ru-RU" root="speak" mode="voice" version="1.0" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0-literals">
<rule id="speak" scope="public">
    <one-of>
        <item>!SYLLABLES</item>
        <item>!SYLLABLES <ruleref uri="#town"/> !SYLLABLES</item>
    </one-of>
</rule>

<rule id="town">
  <one-of>
          <item>москва<tag>27612_1.xml</tag></item>
          <item>нерезиновая<tag>27612_1.xml</tag></item>
          <item>санкт-петербург<tag>26063_1.xml</tag></item>
          <item>петербург<tag>26063_1.xml</tag></item>
          <item>питер<tag>26063_1.xml</tag></item>
          <item>выборг<tag>22892_1.xml</tag></item>
          <item>новосибирск<tag>29634_1.xml</tag></item>
          <item>владивосток<tag>31960_1.xml</tag></item>
          <item>минск<tag>26850_1.xml</tag></item>
          <item>киев<tag>33345_1.xml</tag></item>
          <item>алмаата<tag>36870_1.xml</tag></item>
          <item>алма-ата<tag>36870_1.xml</tag></item>
          <item>алматы<tag>36870_1.xml</tag></item>
          <item>мехико<tag>76680_1.xml</tag></item>
          <item>хельсинки<tag>2974_1.xml</tag></item>
          <item>берлин<tag>10381_1.xml</tag></item>
          <item>бангкок<tag>48454_1.xml</tag></item>
  </one-of>
</rule>
</grammar>

Основное очевидное преимущество распознавания перед DTMF появляется при множественном выборе, когда абоненту нужно выбрать не один пункт из 3-х (отдел продаж, тех.поддержка или бухгалтерия), а, скажем, один из трехсот (список городов или улиц). В данном примере для упрощения я сделал несколько городов, но ничто не мешает вам сделать выбор из сотни.

time.xml– файл с вариантами выбора времени суток. В семантическом теге первая цифра 0-сегодня, 1-завтра; вторая цифра — время суток аналогично параметру tod в xml-файле погоды.

<?xml version="1.0" encoding="utf-8"?>
<grammar xml:lang="ru-RU" root="speak" mode="voice" version="1.0" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0-literals">
<rule id="speak" scope="public">
    <one-of>
        <item>!SYLLABLES</item>
        <item>!SYLLABLES <ruleref uri="#time"/> !SYLLABLES</item>
    </one-of>
</rule>

<rule id="time">
  <one-of>
          <item>сегодня ночью<tag>00</tag></item>
          <item>сегодня утром<tag>01</tag></item>
          <item>сегодня днём<tag>02</tag></item>
          <item>сегодня вечером<tag>03</tag></item>
          <item>завтра ночью<tag>10</tag></item>
          <item>завтра утром<tag>11</tag></item>
          <item>завтра днём<tag>12</tag></item>
          <item>завтра вечером<tag>13</tag></item>
  </one-of>
</rule>
</grammar>

end_next.xml — простая грамматика, состоящая всего из трех айтемов, которая будет использоваться в конце для продолжения работы с приложением или завершения.
<?xml version="1.0" encoding="utf-8"?>
<grammar xml:lang="ru-RU" root="speak" mode="voice" version="1.0" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0-literals">
<rule id="speak" scope="public">
    <one-of>
        <item>!SYLLABLES</item>
        <item>!SYLLABLES <ruleref uri="#check"/> !SYLLABLES</item>
    </one-of>
</rule>

<rule id="check">
    <one-of>
        <item>выбрать город<tag>city_choice</tag></item>
        <item>другое время<tag>time_choice</tag></item>
        <item>завершить<tag>bye</tag></item>
  </one-of>
</rule>
</grammar>

Предзаписанные звуковые файлы


Синтез выгодно использовать для динамически создаваемых фраз и ответов на выборы пользователя. В случае со статическими фразами правильнее записывать их заранее. Этот подход имеет два основных преимущества:
— уменьшается нагрузка на ресурсы синтеза
— предзаписанные файлы можно передавать параметром функции MRCPRecog и использовать режим barge-in (возможность прервать файл и запустить распознавание при начале речи).

Файлы можно записать с помощью функций MRCPSynth и Monitor. В своем приложении мы будем использовать следующие файлы:
city_choice.wav Произнесите название города, погода в котором Вас интересует.
no_input.wav Говорите, пожалуйста, громче.
error_city.wav Извините, непонятно. Повторите название города.
error.wav Я вас не поняла. Пожалуйста, повторите.
time_choice.wav Произнесите погода на какое время Вас интересует. Например, «завтра днем».
end_next.wav Чтобы узнать погоду в другом городе, произнесите «выбрать город». Чтобы узнать погоду на другое время суток произнесите «другое время». Для завершения звонка произнесите «завершить».
bye.wav Спасибо за Ваш звонок. До свидания!
not_found_time.wav Данных о погоде на выбранное время нет. Выберите другое время.

Скрипт для работы с xml-файлом погоды


Создадим AGI-скрипт gismeteo.agi, который будет получать из диалплана имя xml-файла с погодой и распознанное время суток, после чего осуществлять поиск информации о погоде в этом файле.
Для взаимодействия с Asterisk используется Asterisk::AGI, для разбора xml — XML::Simple
#!/usr/bin/perl

use XML::Simple;
use Asterisk::AGI;
use Time::localtime;
use strict;
$|=1;

my $AGI = new Asterisk::AGI;
my %var = $AGI->ReadParse();

#Читаем нужные переменные
my $xml_file=$AGI->get_variable("xml_file");
my $xml_source="/var/www/html/gismeteo/xml/$xml_file";

if ($ARGV[0] eq "city"){    #Если первый аргумент = city, то скрипт используется для поиска названия города
    #Определяем название города по имени xml-файла
    open (LIST, "/var/www/html/gismeteo/agi-bin/get_xml.sh") || die "Ошибка при открытии get_xml.sh";

    my $city="";

    while (<LIST>) {
            if(m/$xml_file/) {
                ($city)=/#(.*)/;
                last;
            }
    }

    close(LIST);
    $AGI->set_variable('city' => $city);
    exit;

} elsif ($ARGV[0] eq "time") {              #Поиск информации о погоде для выбранного дня и времени суток
    #Разбираем семантический тег времени суток
    my @cl_time=$AGI->get_variable("RECOG_INT0")=~/(.)/g;

    #Определяем сегодняшний день недели
    my $present_time=localtime(time());
    my $present_weekday=$present_time->wday;

    #Задаем переменные для последующего использования в фразе
    my @day=('Сегодня','Завтра');
    my @tod=('ночью','утром','днем','вечером');
    my @cloudiness=('Ясно','Малооблачно','Облачно','Пасмурно');
    my %precipitation=('4'=>'Дождь', '5'=>'Ливень', '6'=>'Снег', '7'=>'Снег', '8'=>'Гроза', '9'=>'', '10'=>'Без осадков');

    #Разбираем XML-файл
    my $xmlWeather = new XML::Simple(keeproot => 1,searchpath => ".", forcearray => 1, suppressempty => '');
    my $xmlTown = $xmlWeather->XMLin($xml_source);
    my $xmlData = $xmlTown->{MMWEATHER}[ 0]->{REPORT}[ 0]->{TOWN}[ 0]->{FORECAST};

    my $i=0;

    #Проходим циклом по файлу и ищем данные, удовлетворяющие условию времени
    for ($i=0; $i<4; $i++) {
        print "$xmlData->[$i]->{weekday}, $present_weekday%7+1+$cl_time[0], $xmlData->[$i]->{tod}\n";
        if ($xmlData->[$i]->{weekday}==($present_weekday%7+1+$cl_time[0]) && $xmlData->[$i]->{tod}==$cl_time[1]) {
            $AGI->set_variable('speech_text' => "$day[$cl_time[0]] $tod[$xmlData->[$i]->{tod}] температура воздуха от $xmlData->[$i]->{TEMPERATURE}[ 0]->{min} до $xmlData->[$i]->{TEMPERATURE}[ 0]->{max} градусов. $cloudiness[$xmlData->[$i]->{PHENOMENA}[ 0]->{cloudiness}]. $precipitation{$xmlData->[$i]->{PHENOMENA}[ 0]->{precipitation}}.");
            $AGI->set_priority('found');
            exit;
        }
    }

    #Переходим на приоритет для случая, когда время не найдено
    $AGI->set_priority('not_found');
}

Приложение в extensions.conf


Я предпочитаю писать приложение в отдельном файле и включать его в /etc/asterisk/extensions.conf с помощью include.
Создаем файл gismeteo.conf.

Макрос для распознавания

Для начала напишем макрос, который будет заниматься непосредственно распознаванием:
[macro-recog-gismeteo]
;ARG1 - файл грамматики, ARG2 - звуковой файл, ARG3 - звуковой файл для ошибки распознавания, ARG4 - следующий экстеншн
exten => s,1,MRCPRecog(${GRAMMARS_PATH}/${ARG1},ct=0.20&b=1&f=${SND_PATH}/${ARG2})
exten => s,n(recog),SET(RECOG_HYP_NUM=0)
exten => s,n,SET(RECOG_UTR0=ошибка)

;Передаем результат распознавания NLSML-парсеру
exten => s,n,AGI(NLSML.agi,${QUOTE(${RECOG_RESULT})})

;Проверка на no-input
exten => s,n,GotoIf(${REGEX("Completion-Cause: 002" ${RECOG_RESULT})}?$[${PRIORITY}+1]:check_error)
exten => s,n,MRCPRecog(${GRAMMARS_PATH}/${ARG1},ct=0.20&b=1&f=${SND_PATH}/no_input)
exten => s,n,Goto(recog)

;Если не распознали речь
exten => s,n(check_error),GotoIf($["${RECOG_UTR0}" = "ошибка"]?$[${PRIORITY}+1]:ok)
exten => s,n,MRCPRecog(${GRAMMARS_PATH}/${ARG1},ct=0.20&b=1&f=${SND_PATH}/${ARG3})
exten => s,n,Goto(recog)

;Переходим на нужный приоритет
exten => s,n(ok),Goto(${MACRO_CONTEXT},${MACRO_EXTEN},${ARG4})

Макрос получает в качестве параметров файл грамматики, файл звукового сообщения, файл сообщения об ошибке распознавания и приоритет, на который необходимо перейти при успешном распознавании.
Умеет обрабатывать ошибки распознавания(сообщает об ошибке и просит повторить) и no-input (просит говорить громче, если в канале не было обнаружено речи).
ConfidenceTreshhold=20, что должно быть достаточно, чтобы отсеивать варианты с низкой достоверностью распознавания.
Парсер NLSML.agi получает на вход переменную ${RECOG_RESULT} и в результате работы возвращает в диалплан переменные:
${RECOG_UTR0} — распознанная фраза из грамматики,
${RECOG_INT0} — семантический тег,
${RECOG_CNF0} — уровень достоверности,
${RECOG_SNR0} — уровень соотношения сигнал/шум.

Приложение gismeteo

[gismeteo]
exten => 6853,1,Goto(gismeteo,1) ; Номер, на который мы принимаем звонки

exten => gismeteo,1,Answer()

;Устанавливаем переменные
exten => gismeteo,n,Set(SND_PATH=/var/www/html/gismeteo/sounds)
exten => gismeteo,n,Set(GRAMMARS_PATH=http://192.168.2.103/gismeteo/grammars)
exten => gismeteo,n,Set(AGI_PATH=/var/www/html/gismeteo/agi-bin)

;Распознаем город
exten => gismeteo,n(city_choice),Macro(recog-gismeteo,towns.xml,city_choice,error_city,$[${PRIORITY}+1])

exten => gismeteo,n,SET(xml_file=${RECOG_INT0})

;Передаем данные в AGI-скрипт для поиска названия города
exten => gismeteo,n,AGI(${AGI_PATH}/gismeteo.agi,city)

exten => gismeteo,n,MRCPSynth(<?xml version=\"1.0\"?><speak version=\"1.0\" xml:lang=\"ru-ru\" xmlns=\"http://www.w3.org/2001/10/synthesis\"><voice name=\"Мария8000\">Вы выбрали город ${city}.</voice></speak>)

;Распознаем время
exten => gismeteo,n(time_choice),Macro(recog-gismeteo,time.xml,time_choice,error,agi_check)

;Передаем данные в AGI-скрипт для поиска погоды
exten => gismeteo,n(agi_check),AGI(${AGI_PATH}/gismeteo.agi,time)

;Сообщаем когда выбранное время не найдено и просим повторить
exten => gismeteo,n(not_found),Macro(recog-gismeteo,time.xml,not_found_time,error,agi_check)

;Сообщаем погоду на выбранное время
exten => gismeteo,n(found),MRCPSynth(<?xml version=\"1.0\"?><speak version=\"1.0\" xml:lang=\"ru-ru\" xmlns=\"http://www.w3.org/2001/10/synthesis\"><voice name=\"Мария8000\">${speech_text}</voice></speak>)

;Завершить или продолжить
exten => gismeteo,n,Macro(recog-gismeteo,end_next.xml,end_next,error,$[${PRIORITY}+1])
exten => gismeteo,n,Goto(${RECOG_INT0})

exten => gismeteo,n(bye),Playback(${SND_PATH}/bye)
exten => gismeteo,n,Hangup()

Логика работы приложения

Система: Произнесите название города, погода в котором Вас интересует.
Абонент: Москва
Если город не распознан или сказано слишком тихо, то система выдает соответствующее голосовое сообщение. Если распознавание прошло удачно, то система сообщает результат.
Система: Вы выбрали город Москва.
Система: Произнесите погода на какое время Вас интересует. Например, «завтра днем».
Абонент: Завтра утром.
Если время суток не распознано или сказано слишком тихо, то система выдает соответствующее голосовое сообщение. Если распознавание прошло удачно, то запускается gismeteo.agi, который ищет необходимую информацию в xml-файле погоды. Если информации по данному времени суток в файле нет, например, абонент сегодня вечером произносит «сегодня утром», то он получит сообщение «Данных о погоде на выбранное время нет. Выберите другое время» Если информация найдена, то система сообщает результат.
Система: Завтра утром температура воздуха от 16 до 18 градусов. Ясно. Без осадков.
Система: Чтобы узнать погоду в другом городе, произнесите «выбрать город». Чтобы узнать погоду на другое время суток произнесите «другое время». Для завершения звонка произнесите «завершить».
Фраза «выбрать город» возвращает к началу приложения. Фраза «другое время» позволяет узнать погоду в выбранном городе в другое время суток.
Абонент: Завершить.
Система: Спасибо за Ваш звонок. До свидания!

Мне кажется, решение получилось довольно простым и элегантным. Можно сравнить его с решением, описанным в исходной статье;)
Кроме того, постарался показать многие особенности и «фишки» работы в Астериске, такие как использование NLSML-парсера, упрощение диалплана за счет вынесения распознавания в макрос, использование предзаписанных фраз для barge-in и т.д. Заодно коснулся создания AGI-скриптов для работы с внешними данными. Аналогично с обработкой xml можно обращаться и получать данные из БД или любого другого источника.

В следующей статье хотелось бы затронуть больную тему и описать некоторые проблемы и ограничения Asterisk как платформы, в контексте использования в нем синтеза и распознавания.

Жду ваших вопросов, комментариев, пожеланий.

P.S: Проверить работу приложения можно тут (812)3258848, доб. 6853
P.P.S: Друзья, надеюсь желающих позвонить будет не слишком много и вы не положите корпоративную телефонию))
Tags:
Hubs:
+13
Comments2

Articles

Information

Website
speechpro.ru
Registered
Founded
1990
Employees
201–500 employees
Location
Россия