任何人在工作繁忙的时候都需要一个良好的日程表工具,那么为什么不考虑设计一个支持语音的日程表工具呢?在 VoiceXML 的帮助下,我们可以创建一个能通过说话操纵的日程表。在创建该工具的过程中,我们还将学习如何:
- 创建一个基于菜单的应用程序
- 接收输入
- 将输入写入脚本,用于进一步处理
- 读取数据文件并输出 VXML
语音和音频在 Web 上的应用日益盛行。具体的例子包括目前的各种在线音乐和网络广播。本系列将结合语音和 XML 开发以下这些实用的应用程序:
- 第 1 部分 — 支持语音的 RSS 阅读器
- 第 2 部分 — 支持语音的日历
- 第 3 部分 — 支持语音的 blog 和 Twitter 应用程序
- 第 4 部分 — 支持语音的 Yahoo 搜索应用程序
日程表工作流将使用一个非常简单的结构,至少在最初只有以下两个选项:
- 列出已有约会
- 添加约会
为方便起见,我们还将提供一个不需要特殊类型处理程序的选项。当用户说出 “finish” 时,调用将断开连接。
图 1 显示了主菜单的基本布局,共含有两个选项。
图 1. 应用程序的主菜单
从 图 1 中不难看出,该程序系统相对较为简单。如果您说出 “diary”,那么应用程序将输出当天的所有日程安排。如果您说出 “appointment”,那么应用程序将开始接收输入。
存储日程表信息使用的是 XML 文档,这意味着我们需要能够动态地将 XML 文档作为 VXML 输出。要接收约会信息 — 也就是年月日和约会类型 — 我们可以在 VXML 中建立其整体模型,不过我们还需要一个动态的组件来保存输入。
第二个选项的结构本身是十分简单的;它需要从用户那里接收 — 通过语音或 Dual Tone Multi-Frequency (DTMF) — 日期和时间信息。图 2 显示了其结构的详细信息。
图 2. 接收约会选择
接下来,我们将讨论该系统的具体实现。
日程表主菜单是一个简单的 VXML 文件,它所提供的主要选项将作为应用程序其余部分的开始点。要实现日程表主菜单,我们可以采用多种方式。但是就本例而言,我们将使用选项标记。这种方法可以自动设置关联字段的值,并且将从话筒(Phone pad)接收一个词用于语音识别,和一个 DTMF 音调。
这样,用户可以通过语音命令或使用小键盘进行选择,因此我们必需提供一个提示,用于说出可用选项的组合。
使用用户选择的内容填充字段后,if 结构将做出判断并执行下一步操作。就前两个选项而言,这意味着链接到不同的 URL。对于最后一个选项,断开标记将挂断电话。
清单 1 显示了 VXML 结构的主菜单。
清单 1. Calendar 应用程序的主菜单
<?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>
|
这里还没有涉及用户的身份。但是在企业环境中,我们需要添加一个身份验证级别,让用户输入惟一标识号码(或者说是他的名称)和一个 PIN 号码或密码用于验证条目。然后,我们可以在 VoiceXML 和动态脚本之间传递 ID 号码。
还有,虽然我们将使用 XML 作为信息的存储格式,但如果您拥有一个惟一可用的 ID,那么可以更加方便地使用这个 ID 直接从数据库中提取信息。
将这些考虑放在一边,我们只研究如何使用 VXML 接收新约会条目。
要在已有文件中添加一个约会,我们需要先从用户处获得要添加条目的日期、时间和约会类型信息。
同样,我们可以采用多种方法接收信息。但是对于前 5 个字段(日、月、年、小时、分钟),我们最终需要获取的是数字信息。
字段标记将接收一个 type 属性,该属性指定了要存储信息的类型,并且还可以用于指定接收操作所使用的语法规则。一些输入类型是预先定义的,数值输入(数字)输入类型就是其中之一,该类型将接收最大和最小数值的指定:<field name="Day" type="digits?minlength=1;maxlength=2">。
注意,这些值是数字值,而不是号码,因此用户必须说数字作为输入。尽管这种方式缺乏一些灵活性,但是它可以确保用户获得正确的信息,特别是在指定年时尤为明显,因为年有很多种说法。
我们可以重复这一过程,完成所有 5 个数值的定义,如清单 2 所示。对于各个字段定义,嵌入的提示将被读出,并且系统将等待适当的响应,然后再移至下一个字段。大多数 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 块。该格式让我们能够指定准许的词的发音,以及如何解释它们并在 VoiceXML 字段中将它们呈现为文本或其他字符串。比如说,如果我们想要接收 “meeting” 这个语音并将相应的词指派给该字段,那么可以使用这个方法:[meeting] {<TypeOfMeeting "meeting">}。
我们还可以添加一些额外的选项,如清单 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 中的代码指定了信息目的地的目标字段,以及将写入该字段的最终数据。
用户提供了需要填入字段的信息之后,程序将调用 VoiceXML 中的填充块。对于应用程序,我们将使用它重新指定约会的日期、时间和类型。
当用户说出特定的数据类型时,该信息会将要转换为语音的信息类型告诉语音浏览器的 TTS 系统。比如说,如果我们想使用典型的日期 “17/5/2007”,那么将其读出来可能是 “seventeen-slash-five-slash-two thousand and seven”。这个字符串本身是没有任何意义的。但是,如果 TTS 系统将其识别为日期,那么取而代之可以将该字符串读作 “seventeenth of May two thousand and seven”。
say-as 标记可以通知 TTS 解析器如何读出项目,interpret-as 标记将指示数据类型,而 format 标记将指示信息的规则。比如说 “dmy” 将表示日期的格式为日期-月-年。
我们可以使用相同的标记读出时间(比如说,这样可以将 “11:30” 识别为 “half past eleven”,而不是纯粹的数字)。我们可以使用 VXML 生成这种说明,如清单 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 标记可用于在 VXML 和能够处理信息的脚本之间交换字段信息。应该将要提供的字段用空格隔开放在 namelist 属性中。
然后,语音浏览器会将这些信息转换为标准的 http 字段/值字符串,这样我们便可以解析和提取这些信息,其方法与标准 HTML 页面中通过表单将参数提供给脚本的方法相同。清单 5 中的 VXML 代码将从变量中将响应发送给用于存储约会的脚本。
清单 5. 将输入字段数据提交给外部脚本
<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::DOM 模块加载已有的 XML 。然后,添加新约会并将其写入更新文件,但是我们也可以轻易地使用 Java™、Python 或 PHP 脚本来处理输入。
XML 文件的格式如清单 7 所示。
清单 7. 使用 Perl 脚本读取信息
<diary>
<meeting date="26/3/2007" time="12:30" type="Party"/>
</diary>
|
会议标记包含一切所需的内容,实际数据存储于它的各个属性中。这样可以简化信息的写入和更新。要确保能够接收到有效的数据,并且避免将错误信息写入 XML,我们可以访问其参数并相应地设置 ok 变量。
在这里,我们也可以使用所提供的数据创建一个 DateTime 对象,用于确认所提供数日期的有效性,如果数据无效则将 ok 变量设置为 false。ok 变量的值为 false 将触发脚本输出一个 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 输出。日程列表的末尾有一个返回选项,可以使用户返回主菜单 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
|
清单 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>
|
您可以在更新版本的应用程序中对当前脚本中的众多元素进行改进和扩展。您可以提供一个额外的菜单,让用户可以选择输出的持续时间。比如说,您可以让用户说出以下短语(通过一个额外的 VoiceXML 菜单):
- 今天(Today) — 当天的约会。由于输出和选择将以脚本为依据,因此我们可以确认当天的约会并动态的筛选 XML。
- 明天(Tomorrow)— 第二天的约会。
- 具体日期(Specific date)。
使用上文所介绍的规则,这一过程的基本原理将十分清楚。
在本文中,我们了解了如何扩展 VoiceXML 应用程序与支持它的脚本之间的交互性。此处的关键因素是 submit 标记,它使我们能够将信息提交给脚本,并且其方式与将字段提交给任何普通 Web 脚本的方式一样。这种信息交互方式使在应用程序、已有数据和基于语音的浏览器或接口之间进行交互成为了可能。
务必请阅读下一部分,即本系列的第 3 部分。第 3 部分将介绍如何开发一个简单的博客发布应用程序,该应用程序将使用 VoiceXML 作为输入并将数据保存到在线博客中。您还将了解如何在 VoiceXML 中使用 “tweets”,也就是 Twitter 条目。
| 描述 | 名字 | 大小 | 下载方法 |
|---|---|---|---|
| 第 2 部分示例代码 | x-voicexml-cal.zip | 4KB | HTTP |
学习
- 您可以参阅本文在 developerWorks 全球网站上的 英文原文。
-
参阅本系列的其他部分:
-
在 Java Web 开发框架中创建 VoiceXML 页面,第 1 部分: 使用 Java servlet 和 JSP 生成 VoiceXML(Brett McLaughlin,developerWorks,2006 年 11 月):了解 Java servlet 如何轻松地支持 VoiceXML 应用程序。
-
在 Java Web 开发框架中创建 VoiceXML 页面,第 2 部分: 扩展 Java 驱动的 VoiceXML 应用程序(Brett McLaughlin,developerWorks,2006 年 12 月):学习如何使用 servlet 扩展单页面应用程序。
-
VoiceXML 2.1
规范:了解 Voice Extensible Markup Language 平台实现的共有特性集。
-
IBM XML 认证:看看如何才能成为一名 IBM 认证的 XML 及相关技术的开发人员。
-
XML 技术文档库:developerWorks XML 专区提供了大量技术文章、提示、教程、标准以及 IBM 红皮书。
-
developerWorks 技术事件和网络广播:随时关注技术的最新进展。
-
技术书店: 浏览技术书店获得关于各类技术主题的书籍。
获得产品和技术
-
Rome RSS/Atom syndication:下载这些用来解析、生成和发布 RSS 和 Atom 提要的开放源码 Java 工具和库。
-
XML::FeedPP:从 Perl 的模块存储库 CPAN 获得这个模块。
-
JDOM 库:下载这个用于 Java 编程的基于 DOM 的 XML 解析器。Rome RSS 库需要使用这个库。
-
Voxeo:在这里可以找到大量信息和 VoiceXML 应用程序的解决方案,可以通过传统方式、VoIP 和 Skype 进行访问。
-
IBM 试用软件:使用可从 developerWork 直接下载的试用软件构建您的下一个开发项目。
讨论
- 参与论坛讨论。
-
XML 专区论坛:参与各个以 XML 为中心的论坛。
-
developerWorks blogs:查阅这些 blog 并参与 developerWorks 社区。

Martin Brown 成为职业作家已有八年多的时间。他所撰写的书籍和文章涵盖了各种各样的主题。他擅长的领域包括许多开发语言和平台(Perl、Python、Java、JavaScript、Basic、Pascal, Modula-2、C、C++、Rebol、Gawk、Shellscript、Windows、Solaris、Linux、BeOS、Mac OS/X 等等)以及 Web 编程、系统管理与集成。Martin 是 ServerWatch.com、LinuxToday.com 和 IBM developerWorks 的定期撰稿人,还为 Computerworld、The Apple Blog 和其他站点定期编写博客,同时还是 Microsoft 的主题专家(SME)。您可以通过他的网站 http://www.mcslp.com 与他联系。