Голосовая поддержка в XML: Часть 2. разрабатываем календарь с голосовыми функциями (исходники)

Мартин Браун

Введение

Если хорошая программа-календарь нужна практически каждому, то почему бы не добавить в нее голосовые функции? С помощью VoiceXML мы разработаем календарь, которым можно будет управлять голосом. В процессе создания календаря вы научитесь:

  • Создавать интерфейсы на основе голосовых меню
  • Обрабатывать голосовой ввод
  • Передавать входные данные в скрипты для дальнейшей обработки
  • Преобразовывать данные в формат VXML

Об этой серии

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

  • В первой части: RSS-reader с голосовой поддержкой.
  • Во второй части: календаря с голосовой поддержкой.
  • В третьей части: голосовой программы для работы с блогами и Twitter.
  • В четвертой части: программы для голосового поиска в Yahoo.

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

Алгоритм работы нашего будущего календаря можно представить в виде очень простой схемы. Для начала будет достаточно всего двух пунктов выбора в меню:

  • Вывести список запланированных встреч
  • Добавить новую встречу

Для удобства можно добавить еще третью возможность, которая даже не потребует специальной обработки: как только пользователь произнесет "finish" (закончить), программа завершит голосовое соединение.

Схема работы программы приведена на рисунке 1.

Рисунок. Главное меню календаря
Main application menu

Как видно из рисунка 1, логика работы достаточно проста. Если пользователь произносит "diary" (дневник), управление передается приложению, которое выведет список встреч на сегодня. Если же сказать "appointment" (встреча), то управление получит другое приложение, которое будет ожидать голосового ввода.

Вся информация в календаре будет сохраняться в виде XML-документа, а следовательно, будет необходимо динамически преобразовывать XML-данные в формат VXML. Ввод данных при добавлении новой встречи, а именно: тип встречи, день, месяц и год, можно будет осуществлять непосредственно в виде VXML. При этом потребуется специальный компонент для сохранения вводимой информации.

Схема работы этого компонента также проста. Все что от него требуется - это получить дату и время добавляемой встречи от пользователя. Ввод может быть как речевым, так и в виде сигналов DTFM (Dual Tone Multi-Frequency). Подробности приведены на рисунке 2.

Рисунок 2. Ввод новой записи в календарь
Accepting the appointment selection

Далее рассмотрим реализацию всей системы.

Главное меню календаря

Точкой входа в программу является главное меню, представляющее собой несложный VXML-файл, в котором перечислены основные пункты выбора. Подобный выбор можно реализовать несколькими способами, но в данном случае используется VXML-тег "option", позволяющий сохранить слово, вводимое с помощью голоса или DTFM-сигналов с телефонной клавиатуры в соответствующем поле.

Таким образом, пользователь может произнести голосовую команду, либо ввести ее с клавиатуры. В качестве подсказки ему необходим список доступных пунктов меню. Для озвучивания вариантов выбора служит тег "prompt".

Обработка ввода команды от пользователя осуществляется с помощью блока if. В случае выбора пользователем одного из первых двух вариантов происходит вызов приложения по соответствующему URL. В случае же третьего варианта голосовое соединение завершается на теге disconnect.

VXML-файл, описываюший главное меню, приведен в листинге 1.

Листинг 1. Главное меню календаря в VXML

                
<?xml version="1.0" encoding ="UTF-8"?>

<!DOCTYPE vxml PUBLIC "-//W3C//DTD VOICEXML 2.1//EN" 
             "http://www.w3.org/TR/voicexml21/vxml.dtd">

<vxml version="2.1">

  <form id="MM"> 
    <field name="FMM"> 
      <prompt> 
        Please choose.
        <break strength="medium"/> 
        Press one or say diary for your current diary, 
        press two or say appointment to add a new appointment.
        Say finish if you want to finish the session.
      </prompt> 

      <option value="diary" dtmf="1"> 
        diary
      </option> 

      <option value="appointment" dtmf="2"> 
        appointment
      </option> 

      <option value="finish" dtmf="0"> 
        finish
      </option> 
 

      <filled> 
        <if cond="FMM =='diary'">
          <goto next="dumpdate.cgi"/>
        <elseif cond="FMM =='appointment'"/>
          <goto next="entry.vxml"/>
        <elseif cond="FMM =='save'"/>
          <goto next="savedate.cgi"/>
        <elseif cond="FMM =='finish'"/>
          <prompt>Thank you</prompt>
          <disconnect/>
        </if>
      </filled> 

    </field> 
  </form>
</vxml>

На данном этапе можно особенно не заботиться идентификацией пользователя, однако в реальной системе необходимо предусмотреть некий механизм аутентификации, запрашивающий уникальный номер или имя пользователя, а также пароль или пин-код для проверки его личности. Затем в процессе работы приложения идентификатор пользователя можно будет пересылать между VoiceXML и различными служебными скриптами.

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

Отложив пока соображения по поводу идентификации пользователя, обратим внимание на файл VXML, служащий для добавления новой записи в расписание.

Добавление записи в расписание

Для добавления новой записи нужно сначала получить всю необходимую информацию от пользователя, а именно: тип записи, ее время и дату.

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

У тега "field" предусмотрен атрибут type, задающий тип данных, которые могут быть сохранены в этом поле. Он также может использоваться для указания грамматических правил для проверки введенной информации. Некоторые типы данных предопределены заранее, например, тип "digits" используется для ввода и сохранения чисел. При этом можно контролировать минимальное и максимальное количество цифр в числе: <field name="Day" type="digits?minlength=1;maxlength=2">.

Учтите, что вводятся именно цифры, а не числа, поэтому пользователь должен произносить каждую цифру по отдельности. Это может показаться немного неудобным, но зато гарантирует, что информация будет введена корректно. Это особенно актуально при вводе года, потому как зачастую он может быть произнесен различными способами.

Ввод всех пяти числовых атрибутов производится примерно одинаково, как показано в листинге 2. В каждом случае система сначала проговорит текст-инструкцию, содержащуюся в элементе prompt, а затем будет ожидать ввода данных перед тем, как перейти в следующему полю. Большинство браузеров VoiceXML позволяют автоматически повторить инструкцию, если введенные данные не соответствуют ожидаемому типу данных.

Листинг 2. Получение входных данных

                
<?xml version="1.0" encoding ="UTF-8"?>

<!DOCTYPE vxml PUBLIC "-//W3C//DTD VOICEXML 2.1//EN" 
            "http://www.w3.org/TR/voicexml21/vxml.dtd">

<vxml version="2.1" xmlns="http://www/w3/org/2001/vxml" xml:lang="en-US">

<form id="MyForm">
<prompt>Adding a new appointment.<break time="1000"/></prompt>
<field name="Day" type="digits?minlength=1;maxlength=2">
<prompt>Say the day of the month (using digits). For example, for 12th say one, 
two.</prompt>
</field>

<field name="Month" type="digits?minlength=1;maxlength=2">
<prompt>Say the month (in numbers)</prompt>
</field>

<field name="Year" type="digits?minlength=4;maxlength=4">
<prompt>Say the year (in numbers, using four digits)</prompt>
</field>

<field name="Hour" type="digits?minlength=1;maxlength=2">
<prompt>Say the hour (using the 24-hour clock)</prompt>
</field>

<field name="Minute" type="digits?minlength=1;maxlength=2">
<prompt>Say the minutes</prompt>
</field>

Далее необходимо определить список поддерживаемых типов встреч, который будет использоваться голосовым браузером при распознавании речевого ввода от пользователя. Грамматические правила могут задаваться в нескольких различных форматах, в частности, в текстовом или в виде отдельного XML-фрагмента.

При использовании текстового формата правила помещаются в VXML-файл в виде блока CDATA. Формат позволяет опеределить список распознаваемых слов, а также правила их интерпретации и представления в виде строк при сохранении в полях документа VXML. Например, если при обработке голосового ввода требуется распознавать слово "meeting" (встреча), а затем сохранять значение "meeting" в поле "TypeOfMeeting", то можно использовать следующую конструкцию: [meeting] {<TypeOfMeeting "meeting">}.

Можно добавлять и другие значения в список поддерживаемых слов, например, dentist, doctor или party (см. листинг 3).

Листинг 3. Грамматические правила для типа записи в календаре

                

<field name="TypeOfMeeting">
<prompt>Say the type of appointment. Options are meeting, dentist, 
                                            doctor, party. </prompt>
      <grammar type="text/gsl">
        <![CDATA[[
          [meeting] {<TypeOfMeeting "meeting">}
          [dentist] {<TypeOfMeeting "dentist">}
          [doctor] {<TypeOfMeeting "doctor">}
          [party] {<TypeOfMeeting "party">}
        ]]]>
      </grammar>
</field>

Обратите внимание, что в листинге 3 задаются как сами значения, так и наименования полей в VXML для их сохранения.

Блок "filled" вызывается сразу после того, как пользователь ввел значения всех полей в VXML. В нашем приложении этот блок будет использоваться для голосового подтверждения ввода параметров встречи.

Указание типа данных зачастую помогает модулю TTS (text-to-speech) голосового браузера правильно озвучивать значение того или иного поля. Например, дата, записанная в виде строки "17/5/2007" будет произнесена как "семнадцать-слэш-пять-слэш-две-тысячи-семь", что лишено всякого смысла. Если же модуль TTS поймет, что строка представляет собой дату, то он может проговорить ее как "семнадцатое мая две тысячи седьмого года".

Тег say-as указывает парсеру TTS, как произносить значение данного поля. Он содержит атрибуты interpret-as и format, задающие тип данных и формат их записи соответственно. Например, "dmy" говорит о том, что дата записана в порядке "день, месяц, год".

Аналогичную конструкцию можно использовать для произношения времени. Например, строка "11:30" будет произнесена как "пол-двенадцатого", а не просто как последовательность чисел. Фрагмент VXML, демонстрирующий использование блока filled c элементами say-as, показан в листинге 4.

Листинг 4. Голосовое подтверждение введенных данных

                
        <filled>
          <prompt>
        You have specified a date of:
        <say-as interpret-as="date" format="dmy">
          <value expr="Day"/>/<value 
                  expr="Month"/>/<value expr="Year"/>
        </say-as>
        At:
        <say-as interpret-as="time" format="hm">
          <value expr="Hour"/>:<value expr="Minute"/>
        </say-as>
              <break/>
        </prompt>
        <prompt>Appointment type of 
          <value expr="TypeOfMeeting"/>
        </prompt>

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

Для передачи данных из полей VXML-документа обрабатывающим скриптам служит тег submit. В нем предусмотрен атрибут namelist, в котором можно задать список полей, чьи значения необходимо передать скрипту. Элементы списка разделяются пробелами.

Затем голосовой браузер сформирует стандартную строку http-запроса в виде списка пар "поле=значение", из которой можно извлекать значения параметров c помощью стандартных методов, другими словами, точно так же, как и в случае отправки обычной Web-формы со страницы HTML. В листинге 5 показан фрагмент VXML, вызывающий скрипт для сохранения новой записи в расписании и передающий ему значения соответствующих полей в VXML.

Листинг 5. Передача значений полей VXML-документа во внешний скрипт

                
        <submit 
         next="savedate.cgi" 
         namelist="Day Month Year Hour Minute TypeOfMeeting"
         method="post"/>
        </filled>

        </form>
        
</vxml>

Голосовой интерфейс нашего VoiceXML-приложения можно еще немного улучшить. Например, можно усовершенствовать грамматические правила для ввода дат таким образом, чтобы пользователь мог произносить название месяца. Новое правило приведено в листинге 6.

Листинг 6. Грамматические правила для ввода месяца

                 
<grammar type="text/gsl">
  <![CDATA[[
    [january] {<TypeOfMeeting "1">}
    [february] {<TypeOfMeeting "2">}
    [march] {<TypeOfMeeting "3">}
...
    [december] {<TypeOfMeeting "12">}
  ]]]>
</grammar>

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

Сохранение записи

Встречи, добавляемые в расписание, будут сохраняться специальным скриптом в виде XML. Скрипт получает из формы VoiceXML данные, переданные ему в виде стандартного запроса HTTP. В нашем примере скрипт будет написан на Perl, а существующий XML-файл с расписанием будет загружаться с помощью модуля XML::DOM. После добавления новой записи измененный файл будет записан обратно на диск. Аналогичным образом скрипт может быть реализован на других языках, например, Java™, Python или PHP.

Формат XML-файла показан в листинге 7.

Листинг 7. Пример хранения запланированной встречи в XML

                
<diary>
<meeting date="26/3/2007" time="12:30" type="Party"/>
</diary>

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

Кроме этого, можно дополнительно проверить корректность введенной даты, попробовав преобразовать ее в объект типа DateTime. В случае неудачи следует сбросить значение переменной ok в 0, что в свою очередь повлечет вывод специального фрагмента VXML, перенаправляющего пользователя к файлу addentry.vxml, который повторно запросит информацию о добавляемой записи. Если же введенная информация корректна, то скрипт добавит новую запись в XML-файл расписания и выведет другой фрагмент VXML, перенаправляющий пользователя к главному меню.

Полный текст скрипта показан в листинге 8.

Листинг 8. Сохранение новой встречи в файле XML

                
#!/usr/bin/perl

use CGI qw/:standard/;
use XML::DOM;

print header(-type => 'text/xml');

my $ok = 1;
foreach my $param (qw/Day Month Year Hour Minute TypeOfMeeting/)
{
    $ok = 0 if (!defined(param($param)));
}

if ($ok)
{
    my $parser = new XML::DOM::Parser;
    my $doc = $parser->parsefile ("dates.xml");
    
    my $meeting = $doc->createElement('meeting');
    $meeting->setAttribute('date', sprintf('%s/%s/%s',
                                           param('Day'),
                                           param('Month'),
                                           param('Year')));
    
    $meeting->setAttribute('time', sprintf('%s:%s',
                                           param('Hour'),
                                           param('Minute')));
    
    $meeting->setAttribute('type', param('TypeOfMeeting'));
    
    my $diary = $doc->getElementsByTagName ("diary")->item(0);
    $diary->appendChild($meeting);
    
    open(DATA,">dates.xml");
    print DATA $doc->toString;
    close(DATA);
print <<EOF;
<?xml version="1.0" encoding="UTF-8"?>

<vxml version="2.1">
<form>
<block>
<prompt>Appointment saved.<break time="2000"/></prompt>
<goto next="calmenu.vxml"/>
</block>
</form>
</vxml>
EOF

}
else
{
    print <<EOF;
<?xml version="1.0" encoding="UTF-8"?>

<vxml version="2.1">
<form>
<block>
<prompt>Sorry, there was a problem with your appointment. 
Please try again.<break time="2000"/></prompt>
<goto next="entry.vxml"/>
</block>
</form>
</vxml>
EOF
}

Нам понадобится еще один скрипт для считывания информации из XML-файла, содержащего расписание, и озвучивания текущего списка встреч.

Вывод текущего расписания

Данный скрипт считывает содержимое файла diary.xml и создает VXML-фрагмент для голосового вывода списка запланированных встреч. Сам скрипт очень прост и не содержит ничего, кроме кода генерирования VXML, который вы уже видели ранее. После вывода последней записи пользователь перенаправляется к главному меню. Полный текст скрипта приведен в листинге 9.

Листинг 9. Преобразование текущего расписания в формат VXML

                
#!/usr/bin/perl

use CGI qw/:standard/;
use XML::DOM;

print header(-type => 'text/xml');

my $parser = new XML::DOM::Parser;
my $doc = $parser->parsefile ("dates.xml");

my $nodes = $doc->getElementsByTagName ("meeting");
my $n = $nodes->getLength;

print <<EOF;
<?xml version="1.0" encoding="UTF-8"?>

<vxml version="2.1">
  <form>
<block><prompt>Your current diary.<break 
                         time="2000"/></prompt></block>
EOF

for (my $i = 0; $i < $n; $i++)
{
    my $node = $nodes->item ($i);
    my $daten = $node->getAttributeNode ("date");
    my $timen = $node->getAttributeNode ("time");
    my $typen = $node->getAttributeNode ("type");
    
    my $date = $daten->getValue;
    my $time = $timen->getValue;
    my $type = $typen->getValue;

    print <<EOF;
    <block>
        <prompt>
        <say-as interpret-as="date" format="dmy">$date</say-as>,
      at 
        <say-as interpret-as="time" format="hm">$time</say-as>,
        <break time="500"/>      
      $type
        <break time="1000"/>
        </prompt>
EOF

if ($i == ($n-1))
{
    print '<prompt><break time="1000"/>End of diary. 
Returning to main menu.<break time="2000"/></prompt><goto 
next="calmenu.vxml"/>';
}

print '</block>';
}

print <<EOF;
  </form>
</vxml>
EOF

Пример вывода в формате VXML показан в листинге 10.

Листинг 10. Пример расписания в формате VXML

                
Content-Type: text/xml; charset=ISO-8859-1

<?xml version="1.0" encoding="UTF-8"?>

<vxml version="2.1">
  <form>
  <block>
    <prompt>Your current diary.<break time="2000"/></prompt>
  </block>
  <block>
    <prompt>
      <say-as interpret-as="date" format="dmy">26/3/2007</say-as>,
        at 
      <say-as interpret-as="time" format="hm">12:30</say-as>,
      <break time="500"/>      
        Party
      <break time="1000"/>
    </prompt>
  </block>
  <block>
    <prompt>
      <say-as interpret-as="date" format="dmy">1/3/2007</say-as>,
        at 
      <say-as interpret-as="time" format="hm">12:30</say-as>,
      <break time="500"/>      
        doctor
      <break time="1000"/>
    </prompt>
  </block>    
  <block>
    <prompt>
      <say-as interpret-as="date" format="dmy">2/2/2007</say-as>,
        at 
      <say-as interpret-as="time" format="hm">10:30</say-as>,
      <break time="500"/>      
        party
      <break time="1000"/>
    </prompt>
    <prompt>
      <break time="1000"/>
        End of diary. Returning to main menu.
      <break time="2000"/>
    </prompt>
    <goto next="calmenu.vxml"/>
  </block>  
</form>
</vxml>

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

  • Сегодня: вывод списка встреч на сегодня. Благодаря тому, что VXML-содержимое генерируется динамически скриптом, можно автоматически определить текущую дату и использовать ее для выборки нужных записей в XML.
  • Завтра: вывод списка встреч на завтра.
  • Дата: вывод списка встреч на заданную дату.

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

Заключение

В статье была представлена расширенная схема взаимодействия между приложение VoiceXML и скриптами, выполняющими служебные функции. Ключевым моментом в этом взаимодействии является тег "submit", позволяющий передавать данные из VXML в скрипт аналогично тому, как вызываются любые Web-скрипты. Подобный подход к обмену информацией открывает широкие возможности по связыванию ваших приложений с обычными и голосовыми браузерами или интерфейсами.

Оставайтесь с нами, и в третьей части мы рассмотрим разработку простого приложения, принимающего на вход Voice-XML-данные и сохраняющего их в вашем онлайн-блоге. Кроме того, мы покажем, как использовать VoiceXML для работы с так называемыми "tweets" - записями популярного блог-сервиса Twitter.


Страница сайта http://185.71.96.61
Оригинал находится по адресу http://185.71.96.61/home.asp?artId=17164