安卓语音应用开发-全-

安卓语音应用开发(全)

原文:zh.annas-archive.org/md5/ADB9095CFEE0752E2C9902904FAEC11A

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

能够与计算机交谈的想法长期以来吸引着许多人。然而,直到最近,这看起来还像是科幻小说的内容。现在情况已经改变,拥有智能手机或平板电脑的人可以使用语音在设备上执行许多任务——你可以发送短信、更新日历、设置闹钟,以及提出你之前会输入到搜索框中的问题。在小型设备上,语音输入通常更为方便,尤其是在物理限制使得打字和点击更加困难的情况下。

本书为 Android 设备开发语音应用提供了实用指南,使用谷歌语音 API 进行文本到语音(TTS)和自动语音识别(ASR)以及其他开源软件。尽管有许多书籍涵盖了 Android 编程的通用内容,但没有一个资源全面处理为 Android 开发基于语音的应用程序。

开发语音用户界面与开发更传统界面的许多特性是相同的,但也存在一些特定的要求使得语音应用开发与众不同,开发者进入这一领域时,了解常见的陷阱和困难是很重要的。本书提供了一些入门材料,以覆盖那些可能不为主流计算背景的专业人士所熟悉的方面。然后详细展示了如何从简单的程序逐步构建起复杂的应用程序。通过在书中示例的基础上进行实践,尝试描述的技术,你将能够为你的 Android 应用带来语音的力量,使它们更智能、更直观,提升用户的移动体验。

本书涵盖的内容

第一章,Android 设备上的语音,讨论了如何在 Android 设备上使用语音,并概述了涉及的技术。

第二章,文本到语音合成,涵盖了文本到语音合成技术以及如何使用谷歌 TTS 引擎。

第三章,语音识别,概述了语音识别技术以及如何使用谷歌语音识别引擎。

第四章,简单语音交互,展示了如何构建用户与应用程序可以进行对话的简单交互,以获取一些信息或执行操作。

第五章,表单填充对话框,说明了如何创建与传统网页应用中表单填充类似的语音对话框。

第六章,对话语法,介绍了如何使用语法来解释用户的输入,这些输入超出了单个词和短语。

第七章,多语言和多模态对话,探讨了如何构建使用不同语言和模态的应用程序。

第八章,与虚拟个人助手的对话,展示了如何构建一个支持语音的个人助手。

第九章,进一步探索,展示了如何开发更高级的虚拟个人助手。

您需要这本书的内容

要运行代码示例并开发您自己的应用程序,您需要安装 Android SDK 和平台工具。一个包含必要的 Android SDK 组件以及内置 ADT(Android 开发者工具)版本的 Eclipse IDE 和教程的完整捆绑包可以在developer.android.com/sdk/下载。

您还需要一个 Android 设备来构建和测试示例,因为 Android ASR(语音识别)在虚拟设备(模拟器)上无法工作。

本书的目标读者

本书面向所有对语音应用开发感兴趣的人,包括语音技术和移动计算专业的学生。我们假设您具有一般的编程背景,特别是在 Java 方面。我们还假设您对 Android 编程有一定的了解。

约定

在这本书中,您会发现多种文本样式,这些样式区分了不同类型的信息。以下是一些样式示例,及其含义的解释。

文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 处理程序会以下列形式显示:"以下代码行创建了一个实现onInitListener接口的onInit方法的TextToSpeech对象。"

代码块设置如下:

TextToSpeech tts = new TextToSpeech(this, new OnInitListener(){ 
    public void onInit(int status){ 
       if (status == TextToSpeech.SUCCESS) 
             speak("Hello world", TextToSpeech.QUEUE_ADD, null); 
    }
}

当我们需要引起您对代码块中特定部分的注意时,相关的行或项目会以粗体显示:

Interpret field i:
       Play prompt of field i
       Listen for ASR result
       Process ASR result:
               If the recognition was successful, then save recognized
	       keyword as value for the field i and move to the next field
               If there was a no match or no input, then interpret field i
               If there is any other error, then stop interpreting
Move to the next field:
       If the next field has already a value assigned, then move to the next one
       If the last field in the form is reached,thenendOfDialogue=true

新术语重要词汇以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的,会在文本中以这样的形式出现:“请说出专辑名称的一个词”

注意

警告或重要注意事项会以这样的框显示。

提示

提示和技巧会像这样出现。

读者反馈

我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢或可能不喜欢的地方。读者的反馈对我们来说非常重要,帮助我们开发出您真正能够充分利用的标题。

如果您想要给我们发送一般性反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上有专业知识,并且有兴趣撰写或参与书籍编写,请查看我们在www.packtpub.com/authors上的作者指南。

客户支持

既然您已经自豪地拥有了一本 Packt 图书,我们有许多方法可以帮助您充分利用您的购买。

下载示例代码

您可以从您的账户中下载您购买的所有 Packt 图书的示例代码,访问地址为www.packtpub.com。如果您在别处购买了这本书,可以访问www.packtpub.com/support进行注册,我们会将文件通过电子邮件直接发送给您。

图书网页

本书有一个网页lsi.ugr.es/zoraida/androidspeechbook,其中包含额外的资源,包括练习和项目的想法,进一步阅读的建议,以及有用的网页链接。

勘误

尽管我们已经竭尽全力确保内容的准确性,但错误仍然会发生。如果您在我们的书中发现了一个错误——可能是文本或代码中的错误——如果您能将其报告给我们,我们将不胜感激。这样做可以节省其他读者的时间,并帮助我们在后续版本中改进这本书。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情。一旦您的勘误被验证,您的提交将被接受,勘误将在我们网站的相应标题下的勘误部分上传或添加到现有的勘误列表中。任何现有的勘误都可以通过从www.packtpub.com/support选择您的标题来查看。

盗版

互联网上版权材料的盗版是一个所有媒体都面临的持续问题。在 Packt,我们非常重视保护我们的版权和许可。如果您在互联网上以任何形式遇到我们作品非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您发现了疑似盗版材料,请通过<copyright@packtpub.com>联系我们,并提供相关链接。

我们感谢您保护我们的作者,以及我们为您带来有价值内容的能力。

问题

如果您在书的任何方面遇到问题,可以通过<questions@packtpub.com>联系我们,我们会尽力解决。

第一章:安卓设备上的语音

你是否想过创建可以在你自己的安卓设备上运行的语音应用;可以与你对话并对你回应的应用?本章介绍了如何使用谷歌的开源 API 在安卓设备上进行文本语音合成和语音识别。在简要概述了语音用户界面VUIs)的世界之后,本章概述了交互式语音应用(或虚拟个人助手)的组成部分。

到本章结束时,你应该对使用谷歌免费提供的资源创建基于语音的应用所需的条件有一个很好的了解。

在安卓设备上使用语音

安卓设备提供了内置的语音识别和文本语音转换功能。以下是一些基于语音的安卓应用实例:

语音识别

通过语音识别,安卓设备用户可以在需要文本输入的任何文本框中进行语音输入,例如电子邮件、短信和搜索。键盘控制包含一个带有麦克风符号的按钮和两个表示语言输入设置的字母,用户可以更改这些设置。按下麦克风按钮后,会弹出一个窗口提示用户现在说话。用户的口语输入会自动转录成文字。然后用户可以决定如何处理转录的文本。

由于一方面使用了大规模的基于云计算的语音识别资源,另一方面设备通常紧靠用户嘴边以获得更可靠的声学信号,小型设备上的语音听写准确率已显著提高。语音听写的主要挑战之一是输入不可预测——用户可以说字面上的任何内容——因此需要一个大的通用词汇表来覆盖所有可能的输入。其他挑战包括处理背景噪音、含糊的发音和不熟悉的口音。

文本语音转换

文本语音转换(TTS)用于将文本转换为语音。各种应用可以利用 TTS。例如,通过辅助功能选项可用的 TalkBack,使用 TTS 帮助盲人和视力受损的用户描述被触摸、选择和激活的项。TalkBack 还可以用于在 Google Play Books 应用中阅读书籍。TTS 功能在 Android Kindle 以及谷歌地图上提供逐步驾驶指令时也可用。还有大量第三方应用使用 TTS,也有可用的替代 TTS 引擎。

语音搜索

语音搜索在安卓设备上提供与传统的谷歌搜索相同的功能,不同之处在于用户不是输入查询,而是说出查询。用户可以通过谷歌搜索小部件中的麦克风使用语音搜索。在语音搜索中,识别的文本被传递给搜索引擎,并以与输入查询相同的方式执行。

语音搜索的一个新功能是,除了返回一系列链接外,还会返回一个对查询的语音回应。例如,对于问题“埃菲尔铁塔有多高?”,应用会回答:“埃菲尔铁塔高 324 米。”还可以使用代词提出后续问题,例如,“它是何时建造的?”。这种附加功能是通过将谷歌的知识图谱——一个被谷歌使用的知识库——与其会话搜索技术相结合,提供更具对话式的交互风格来实现的。

安卓语音动作

用户也可以通过谷歌搜索小部件中的麦克风访问安卓语音动作。语音动作允许用户使用语音命令控制他们的设备。语音动作需要匹配特定结构的输入,如下面来自谷歌网页的列表所示:www.google.co.uk/intl/en_uk/mobile/voice-actions/。注意:带有*的项目是可选的。斜体字是需要说出的词语。

语音动作 结构 示例
发送短信 给[收件人][信息]*发送短信 给艾莉森·米勒发送短信,内容为“我迟到了。我将在大约 9 点回家”
给商家打电话 给[商家名称][位置]*打电话 给伦敦的 Soho 比萨店打电话
查看地图 [地址/城市]的地图 伦敦的地图
谷歌搜索 [你的查询] 日落时分的巨石阵图片
获取方向 导航至[地址/城市/商家名称] 导航至伦敦的英国博物馆或导航至 24 号磨坊街
给联系人打电话 给[联系人姓名][电话类型]*打电话 给艾莉森·米勒家里打电话
访问网站 前往[网站] 前往维基百科

语音动作中的结构允许它们映射到设备上可用的动作。例如,关键词call表示电话通话,而关键短语go to表示要启动的网站。需要额外的处理来提取动作的参数,如联系人姓名网站

虚拟个人助手

最激动人心的基于语音的应用之一是虚拟个人助手(VPA),它像一个私人助手一样执行一系列任务,如查找有关当地餐馆的信息;执行涉及设备上应用程序的命令,例如使用语音设置闹钟或更新日历;以及进行一般性对话。至少有 20 个 VPA 可用于 Android 设备(请参阅本书的网页)尽管最著名的 VPA 是 Siri,自 2011 年起它就已经在 iPhone iOS 上可用。你可以在苹果公司的网站上找到与 Siri 的互动示例,这些示例与 Android VPAs 执行的操作类似 www.apple.com/uk/ios/siri/。许多 VPA,包括 Siri,都被赋予了个性,并能够以幽默的方式回应恶搞问题和可疑的输入,从而增加了它们的娱乐价值。可以在www.sirifunny.com查看示例以及 YouTube 上的众多视频剪辑。

值得一提的是,以下解释的许多技术具有与 VPAs(虚拟个人助手)某些相似的特点:

对话系统,在学术研究中有着悠久传统,其基于开发能够以自然语言(最初是文本,但最近更多是语音)与人类交流的系统的愿景。最初系统关注于获取信息,例如航班时间或股票报价。第二代系统允许用户进行某种形式的交易,如银行业务或预订旅行,而较新的系统则致力于故障排除,例如指导用户在设置设备时遇到的困难。实现对话系统使用了各种各样的技术,包括基于规则和统计的对话处理。

语音用户界面VUIs),与对话系统相似,但更侧重于商业部署。这类系统通常专注于特定用途,如呼叫路由、电话簿查询和交易对话,例如旅行、酒店、航班、汽车租赁或银行余额查询。许多当前的 VUIs 是使用基于 XML 的 VoiceXML 标记语言设计的。VoiceXML 脚本随后在语音浏览器上解释,该浏览器还提供所需的语音和电话功能。

聊天机器人,传统上被用来模拟人类对话。最早的聊天机器人可以追溯到 20 世纪 60 年代,约瑟夫·魏森鲍姆(Joseph Weizenbaum)编写的著名 ELIZA 程序模拟了一位罗杰斯心理治疗师——常常以令人信服的方式。近年来,聊天机器人在教育、信息检索、商业、电子商务以及自动化的客服中心等领域得到了应用。聊天机器人使用复杂的模式匹配算法来匹配用户的输入并检索适当的回应。尽管大多数聊天机器人是基于文本的,但基于语音的聊天机器人正在逐渐出现(详见第八章,与虚拟个人助手的对话)。

具身对话代理ECAs),是计算机生成的动画角色,结合面部表情、身体姿态、手势和语音,提供一种丰富的通信渠道。通过增强面对面互动的视觉维度,具身对话代理可以显得更加值得信赖和可信,同时也更有趣和娱乐性。具身对话代理已被用于诸如互动语言学习、虚拟培训环境、虚拟现实游戏节目以及互动小说和讲故事系统等应用中。它们越来越多地被用于电子商务和电子银行,以提供友好和有帮助的自动化帮助。例如,在宜家网站上的代理 Anna,请访问www.ikea.com/gb/en/

虚拟个人助手与这些技术不同之处在于,它们允许用户使用语音执行移动设备上许多可用的功能,例如发送短信、查看和更新日历、或设置闹钟。它们还提供了访问网络服务的功能,例如寻找餐厅、跟踪快递、预订航班,或使用诸如知识图谱、Wolfram Alpha 或维基百科等信息服务。由于它们可以访问设备上的上下文信息,如用户的位置、时间和日期、联系人以及日历等,VPA 可以提供与用户位置和偏好相关的餐厅推荐等信息。

设计和开发语音应用

语音应用设计与软件设计的一般特性有许多共同之处,但也存在一些独特的方面,特别是对于语音界面——例如,处理语音识别始终不可能达到 100%准确,因此与使用图形用户界面(GUI)输入相比,其可靠性较低。另一个问题是,由于语音是短暂的,特别是在没有视觉显示的设备上,与 GUI 应用相比,对用户的记忆提出了更高的要求。

语音应用的可使用性受到许多因素的影响。为了确定系统的需求,重要的是要进行广泛的使用案例分析,考虑以下问题:应用是否替代或补充现有应用;语音是否适合作为输入/输出的媒介;应用将提供的服务类型;将使用该应用的用户类型;以及应用的通用部署环境。

为什么选择谷歌语音?

以下是我们使用谷歌语音的原因:

  • Android 设备的普及:“截至 2012 年第三季度,Android 在全球智能手机市场的份额为 75%,总共有 75 亿台设备被激活,每天有 150 万台设备被激活。”(来自www.idc.com/getdoc.jsp?containerId=prUS23771812 2013 年 9 月 7 日检索)。

  • Android SDK 是开源的:Android SDK 是开源的,这使得它比其他一些操作系统更容易被开发者和爱好者用于创建应用。任何人都可以使用如 Eclipse 这样的免费开发环境开发自己的应用,并将其上传到 Android 设备上,供个人使用和享受。

  • 谷歌语音 API:谷歌语音 API 在 Android 设备上免费使用。这意味着对于希望在不投资昂贵的商业可用替代品的情况下尝试语音的开发者来说,语音 API 非常有用。由于谷歌雇佣了许多顶尖的语音科学家,他们的语音 API 在性能上可以与商业提供的相媲美。

提示

你也可以尝试…

Nuance NDEV Mobile 支持多种语言的文本语音合成和语音识别,并提供 PhoneGap 插件,使开发人员能够在不同的平台上实现他们的应用(dragonmobile.nuancemobiledeveloper.com)。

AT&T 语音混合应用支持基于语音的应用开发,并使用 W3C 标准的语音识别语法。

构建虚拟个人助理需要什么?

下图展示了构建一个支持语音的虚拟个人助理所需的各个组件。

构建虚拟个人助理需要什么?

对于一个虚拟个人助理(VPA)的基本要求是它能够说话和理解语言。文本到语音的合成,提供了说话的能力,在第二章,文本到语音合成中进行讨论,而语音识别则在第三章,语音识别中介绍。然而,尽管这些功能对于带语音功能的助手来说是根本性的,但它们还不够。参与对话并与网络服务和设备功能连接的能力也是个人助理的基础。要做到这些,VPA 需要以下能力:

  • 一种控制对话的方法,确定谁应该采取对话的主动权以及他们应该涵盖哪些话题。在实际中,这可以通过一次性的交互简化,用户只需说出他们的查询,应用程序就会响应。一次性交互在第四章,简单的语音交互中介绍。系统引导的对话,即应用程序提出一系列问题——如在基于网络的表单填写(例如,预订酒店或租车)中,这在第五章,表单填写对话中进行讨论。

  • 识别用户输入后对其进行解释的方法。这是口语语言理解组件的任务,它除了提供其他功能之外,还提供了一个语义解释,表示用户所说内容的含义。由于在许多商业系统中,输入被限制为单个单词或短语,因此解释相对简单。两种不同的方法将在第六章,对话语法中进行说明:如何创建一个手工制作的语法,涵盖用户可能说出的单词和短语;以及如何使用统计语法来涵盖更广泛的输入并提供更健壮的解释。如果语音输入和输出不可能或性能不佳,它还提供了不同的模态。如果需要,VPA 还应该具备使用不同语言的能力。这些主题在第七章,多语言和多模态对话中进行讨论。

  • 确定相关动作并生成适当响应。这些对话管理和响应生成方面的内容在第七章,多语言和多模态对话中以及第八章,与个人虚拟助理的对话中进行描述。

在第二章和第三章中介绍的基本文本到语音合成和语音识别技术的基础上,第 4-8 章涵盖了一系列技术,使开发者能够进一步使用这些基本技术,并使用谷歌语音 API 创建基于语音的应用程序。

概述

本章节为 Android 设备上的语音技术提供了引论。我们研究了目前在 Android 设备上可用的各种语音应用程序。我们还探讨了为什么我们决定将重点放在谷歌语音 API 作为开发者的工具上。最后,我们介绍了创建虚拟个人助理所需的主要技术。这些技术将在本书的剩余章节中进行讲解。

在下一章中,我们将向您介绍文本到语音合成(TTS),并展示如何使用谷歌 TTS API 开发能够发音的应用程序。

第二章:文本到语音合成

你是否曾好奇过,你的移动设备是如何大声朗读你最喜欢的电子书或最后的电子邮件的?在本章中,你将了解文本到语音合成技术(TTS),以及如何使用谷歌 TTS 引擎开发能够发声的应用程序。涵盖的主题包括:

  • 文本到语音合成技术

  • 谷歌文本到语音合成

  • 使用文本到语音合成开发应用程序

到本章结束时,你应该能够在 Android 设备上开发使用文本到语音合成的应用程序。

介绍文本到语音合成

文本到语音合成(通常缩写为 TTS)是一种技术,它可以将书面文本转换为语音。TTS 已经被广泛用于为视障人士提供屏幕阅读,同时也用于有严重言语障碍的用户。或许最著名的语音合成技术用户是物理学家斯蒂芬·霍金,他患有运动神经元病,当他的言语变得无法理解时,使用 TTS 作为他的语音。在单词预测技术的帮助下,他能够构建一个句子,然后将其发送到内置的 TTS 系统(更多信息请参见:www.hawking.org.uk/the-computer.html)。

TTS 还广泛应用于用户手或眼忙碌的情况下,例如,在驾驶导航系统时,车辆沿着路线行进时,系统会朗读方向。TTS 的另一个广泛用途是在公共广播系统中,例如在机场或火车站。TTS 还用于基于电话的呼叫中心应用程序,以及一般语音对话系统,用于朗读系统的提示,并与网站上的对话头像结合使用,以提供客户帮助和支持。

TTS 系统的质量对用户的感知有很大影响。用户可能会对听起来机械的系统或错误发音如地名或地址等单词的系统感到烦恼。然而,只要 TTS 的输出可以理解,这至少应该能允许系统充分地执行功能。

文本到语音合成技术

文本到语音合成主要有两个阶段:

  • 文本分析,即对要合成的文本进行分析和准备,以便进行口语输出。

  • 波形生成,即分析后的文本被转换为语音。

文本分析阶段可能会遇到很多问题。例如,单词staring的正确发音是什么?是基于单词star + ing的组合还是stare + ing的组合?要确定这个问题的答案需要对单词结构进行复杂分析;在这种情况下,需要确定像stare这样的单词的词根形式如何通过添加如ing这样的后缀而改变。

还有一些单词根据它们在特定句子中的使用具有不同的发音。例如,作为动词的livegive押韵,但作为形容词时与five押韵。词性还会影响单词内的重音分配;例如,名词record的发音为'record(重音在第一个音节上),作为动词时为re'cord(重音在第二个音节上)。

另一个问题涉及到将数字值转换成适合语音输出的形式(这被称为规范化)。例如,项目12.9.13如果它代表一个日期,就不应该读作twelve dot nine dot thirteen,而应该是December 9th, two thousand thirteen。需要注意的是,使用 Google TTS API 的应用程序开发者无需担心这些问题,因为这些问题已经内置到 TTS 引擎中了。

转向波形生成,早期系统中使用的主要方法要么是发音合成,试图模拟人类产生语音的物理过程,或者是共振峰合成,它模拟声学信号的特征。

现在使用的则是拼接语音合成,在这种合成中,预先录制的语音单元存储在语音数据库中,在生成语音时选择并连接这些单元。这些单元的大小各不相同;单个声音(或音素),相邻的声音对(双音素),这会产生更自然的输出,因为一个音素的发音会根据周围音素而变化;音节、单词、短语和句子;已经开发出复杂的算法来选择最佳的候选单元链,并将它们平滑地连接起来以产生流畅的语音。一些系统的输出常常与真实的人类语音无法区分,特别是在有效使用语调的情况下。语调包括语调、音高、响度、速度和节奏,用于传达意义上的差异以及态度。

使用预先录制的语音而不是 TTS

尽管近年来 TTS 的质量有了显著的提高,但许多商业企业仍然倾向于使用预先录制的语音,以确保输出高质量。通常被称为声音天才的专业艺术家被雇佣来录制系统的提示音。

预录提示的缺点是它们不能用于输出文本不可预测的地方——如在阅读电子邮件、短信或新闻的应用程序中,或者在客户名单不断添加新名字的应用程序中。即使文本可以被预测,但涉及大量组合——如在机场的航班公告中——输出元素的不同部分必须从预录片段拼接起来,但在许多情况下,结果却是生硬和不自然的。另一种情况是可能需要提供其他语言的输出。可以聘请声音人才以各种语言录制输出,但对于更大的灵活性,使用不同语言版本的 TTS 可能成本更低,且足够满足目的。

关于 TTS 与预录语音的问题,已经进行了大量的研究。例如,请参阅James R. Lewis所著的《实用语音用户界面设计》,CRC Press

使用谷歌文本语音合成

TTS 自 Android 1.6(API 级别 4)起在 Android 设备上可用。谷歌 TTS API(包android.speech.tts)的组件在developer.android.com/reference/android/speech/tts/package-summary.html有文档记录。接口和类被列出,点击这些可以获取更多详细信息。

启动 TTS 引擎

启动 TTS 引擎涉及创建TextToSpeech类的一个实例以及当 TTS 引擎初始化时将执行的方法。通过一个名为OnInitListener的接口来检查 TTS 是否已初始化。如果 TTS 初始化完成,将调用onInit方法。

下面的代码行创建了一个实现onInitListener接口的onInit方法的TextToSpeech对象。

TextToSpeech tts = new TextToSpeech(this, new OnInitListener(){ 
    public void onInit(int status){ 
        if (status == TextToSpeech.SUCCESS) 
              speak("Hello world", TextToSpeech.QUEUE_ADD, null); 
    }
}

提示

下载示例代码

你可以从你在www.packtpub.com的账户下载你购买的所有 Packt 图书的示例代码文件。如果你在其他地方购买了这本书,可以访问www.packtpub.com/support注册,文件会直接通过电子邮件发送给你。

你也可以访问这本书的网页:lsi.ugr.es/zoraida/androidspeechbook

在示例中,当 TTS 正确初始化后,将调用speak方法,该方法可能包括以下参数:

  • QUEUE_ADD:新条目被放置在播放队列的末尾。

  • QUEUE_FLUSH:播放队列中的所有条目被丢弃,并由新条目替换。

由于某些设备的存储空间有限,可能并非所有支持的语言实际上都安装在特定设备上。因此,在创建 TextToSpeech 对象之前,检查特定语言是否可用非常重要。这样,如有必要,可以下载并安装所需的特定语言资源文件。这是通过发送带有 ACTION_CHECK_TTS_DATA 动作的 Intent 来实现的,它是 TextToSpeech.Engine 类的一部分,如下面的代码所示:

  Intent intent = newIntent(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
  startActivityForResult(intent,TTS_DATA_CHECK);

如果语言数据已经正确安装,onActivityResult 处理程序将接收到 CHECK_VOICE_DATA_PASS,这时我们应该创建 TextToSpeech 实例。如果数据不可用,将执行 ACTION_INSTALL_TTS_DATA 动作,如下面的代码所示:

Intent installData = new Intent (Engine.ACTION_INSTALL_TTS_DATA);
startActivity(installData);

你可以在 TTSWithIntent 应用程序中查看完整代码,该应用程序在代码捆绑包中提供。

使用 Google TTS 开发应用程序

为了避免在多个地方重复代码,并能够随着我们过渡到更复杂的应用程序而专注于新部分,我们将最常使用的 TTS 功能封装到一个名为 TTSLib 的库中(请参阅源代码中的 sandra.libs.tts),该库在不同的应用程序中使用。

TTS.java 类是按照单例设计模式创建的。这意味着此类只能有一个实例,因此使用该库的应用程序将使用一个单一的 TTS 对象来合成所有消息。这具有多种优势,例如优化资源并防止开发者在同一应用程序中无意创建多个 TextToSpeech 实例。

TTSWithLib 应用程序 – 读取用户输入

下图显示了此应用程序的启动屏幕,用户在此屏幕上输入文本,选择语言,然后按下按钮以使设备开始或停止阅读文本。默认情况下,选中选项是设备中的默认语言,如下面的屏幕截图所示:

TTSWithLib 应用程序 – 读取用户输入

TTSWithLib.java文件中的代码主要初始化视觉用户界面中的元素,并控制所选语言(setLocaleList方法),以及当用户按下说话setSpeakButton)和停止setStopButton)按钮时要执行的操作。从展示的代码中可以看出,主要功能是调用TTSLib库中TTS.java文件里的相应方法。在TTS.java中(参见代码包中的TTSLib项目),有三个名为setLocale的方法用于设置地区。第一个方法接收两个参数,分别对应语言和国家代码。例如,对于英式英语,语言代码是EN,国家代码是GB;而对于美式英语,它们分别是ENUS。第二个方法仅设置语言代码。第三个方法不接收任何参数,仅设置设备的默认语言。可以观察到,如果第一个或第二个方法的任何参数为 null,则会调用第二个和第三个方法。

其他重要方法是负责开始(speak方法)和停止(stop方法)合成,而shutdown方法释放 TTS 引擎使用的本地资源。调用shutdown方法是一个好习惯,我们在调用活动的onDestroy方法中执行它;例如,在TTSDemo.java文件中)。

TTSReadFile 应用 – 大声朗读文件

文本到语音合成的更现实场景是朗读一些文本,特别是在用户的眼和手忙碌时。与上一个示例类似,应用程序检索一些文本,用户按下说话按钮来听它。提供了一个停止按钮,以防用户不想听完全部文本。

这种应用程序的一个潜在用例是用户访问网络上的文本,例如新闻条目、电子邮件或体育报道。这需要额外的代码来访问互联网,这超出了当前应用程序的范围(例如,参见第五章中的 MusicBrain 应用程序,表单填充对话框)。因此,为了简化问题,文本预先存储在Assets文件夹中,并从那里检索。将文本从其他来源检索并传递给 TTS 朗读留给读者作为一个练习。以下截图显示了打开屏幕:

TTSReadFile 应用 – 大声朗读文件

TTSReadFile.java文件与TTSWithLib.java文件类似。如代码所示,主要区别在于它使用英语作为默认语言(与存储的文件匹配)并从文件中获取文本,而不是从用户界面(参见speakbuttononClickListener方法和代码包中的getText方法)。

提示

在书中详细讨论了一些更高级的问题:《专业 Android™传感器编程,Greg Milette 和 Adam Stroud,Wrox,第十六章》。有一些方法可以根据特定设备上可用的内容选择不同的声音。例如,TTS API 提供了额外的帮助您播放不同类型文本的方法。

总结

本章节展示了如何使用谷歌 TTS API 在设备上实现文本到语音的合成。首先提供了文本到语音合成背后的技术概览,随后介绍了谷歌 TTS API 的各个元素。通过两个示例演示了文本到语音合成的基础知识。在后续章节中,将开发更为复杂的方法。

下一章节将处理语音硬币的另一面:语音到文本(或语音识别)。

第三章:语音识别

你是否曾在设备上点击多个菜单和选项,直到你能够完成你想要的操作?你是否希望只需说几个字就能让它工作?本章探讨的是自动语音识别ASR),它将说出的单词转换为书面文字的过程。涵盖的主题包括以下内容:

  • 语音识别技术

  • 使用谷歌语音识别

  • 利用谷歌语音识别 API 开发应用

到本章结束时,你应该能够很好地理解在应用中使用语音识别所涉及的问题,并应该能够使用谷歌语音 API 开发简单的应用程序。

语音识别技术

以下是语音识别的两个主要阶段:

  • 信号处理:这一阶段包括将麦克风录入的单词通过模拟-数字转换器ADC)转换成计算机可以处理的数字数据。ADC 处理数字数据以去除噪音,并执行其他如回声消除等过程,以便能够提取对语音识别有关键作用的特点。

  • 语音识别:信号被分割成微小的片段,与待识别语言的音素进行匹配。音素是语音的最小单位,大致相当于字母表中的字母。例如,单词"cat"中的音素是 /k/, /æ/, 和 /t/。在英语中,例如,大约有 40 个音素,这取决于所讲英语的变体。

在语音识别中最成功的方法是统计建模语音,这样处理的结果是一系列猜测(或假设)用户可能说的话,并根据计算出的概率进行排序。这一统计建模使用复杂的概率函数。其中最常用的模型是隐马尔可夫模型,但神经网络也被使用,有时还使用这两种方法的混合过程。这些模型通过使用大量训练数据的训练过程进行调优。通常,需要使用数百到数千小时的音频来处理人类语音的变异性和复杂性。结果是产生了一个表示语言中声音和单词不同发音方式的声学模型

仅仅依靠声学模型对于高性能的语音识别是不够的。语音识别系统的另一个部分是另一个统计模型,即语言模型。这个模型包含了关于允许的单词序列和给定序列中哪些单词更可能出现的知识。例如,尽管从声学角度来说,totwo听起来是一样的,但在短语I went to the shops中前者更可能出现,在短语I bought two shirts中则是后者。语言模型有助于返回每个短语中正确的单词。

语音识别的输出是根据识别器对识别正确性的置信度(从 0 到 1 的区间)排列的一系列识别结果(称为N-best 列表)。接近 1.0 的值表示对识别正确性有很高的置信度,而接近 0.0 的值表示置信度较低。N-best 列表和置信度分数很有用,因为可能的情况是,首选识别的字符串并不是用户实际所说的内容,而其他选项可能提供更合适的结果。置信度分数也很有用,因为它们可以为决定系统应该如何继续提供依据,例如,是否确认已识别的短语。

使用谷歌语音识别

自从 Android 2.1(API 级别 7)起,语音识别服务就已经在 Android 设备上可用。其中一个可以使用识别功能的地方就是 Android 键盘上的麦克风图标。点击麦克风按钮激活谷歌语音识别服务,如下面的截图所示。红色的麦克风和文字提示表明系统正在等待接收语音:

使用谷歌语音识别

如果没有检测到语音,系统会生成一个重新提示对话框,要求用户再次尝试说话。另一种可能是无法为用户的语音输入找到合适的匹配项。在这种情况下,屏幕会显示未找到匹配项的消息。最后,当没有可用的网络连接时,会显示连接问题的消息。

可以通过设置 | 语言和输入 | 语音 | 语音搜索调整内置语音识别服务的参数,或者根据设备的不同,点击屏幕右上角的工具图标。会展示出多种可用的语言。检查语言的可用性以及选择语言也可以通过编程实现。这将在第七章,多语言和多模态对话中进行介绍。

使用谷歌语音识别 API 开发应用

谷歌语音 API(包android.speech)的组件在developer.android.com/reference/android/speech/package-summary.html进行了文档化。这里列出了接口和类,点击它们可以获得更多详细信息。

在 Android 设备上有两种进行语音识别的方法:仅基于RecognizerIntent的方法,或者创建SpeechRecognizer的实例。前者提供了一个易于编程的机制,通过启动 Intent 类并处理其结果,可以创建使用语音识别的应用程序。按照此方案的应用程序将显示一个对话框,为用户提供关于 ASR 是否准备就绪或在识别过程中出现不同错误的信息反馈。使用SpeechRecognizer为开发人员提供了关于识别相关事件的不同通知,从而允许更细致地处理语音识别过程。这种方法不会显示对话框,为开发人员在应用程序 GUI 上提供更多控制。

在以下章节中,我们将介绍两个应用程序(ASRWithIntentASRWithLib),它们识别用户说的话并以 N-best 列表的形式展示识别结果及置信度得分。实际上,它们是相同的应用程序,但是它们是按照前面描述的两种不同方法开发的:ASRWithIntent使用RecognizerIntent方法,而ASRWithLib使用我们在 ASRLib 库中编程的SpeechRecognizer方法(代码包中的sandra.libs.asr.asrlib)。

带有 Intent 的 ASR 应用

这个简单的应用程序说明了以下内容:

  1. 用户选择语音识别的参数。

  2. 用户按下按钮并说一些话。

  3. 识别出的词语及其置信度得分以列表形式显示。

在打开的屏幕上,有一个带有消息按下按钮说话的按钮。当用户按下按钮时,将启动语音识别功能,使用用户选择的参数。激活语音识别实例并说出贝尔法斯特的天气如何的词组的结果在下图中显示:

带有 Intent 的 ASR 应用

用户按下启动语音识别的按钮是通过引用asrwithintent.xml中指定的按钮设置的,你可以在代码包中找到它(在ASRWithIntent项目的res/layout文件夹中):

//Gain reference to speak button 
Button speak = (Button) findViewById(R.id.speech_btn); 
//Set up click listener 
speak.setOnClickListener(
       new View.OnClickListener() { 
       @Override 
       public void onClick(View v) { 
             //Speech recognition does not currently work on simulated devices, 
             //it is the user attempting to run the app in a simulated device 
             //they will get a Toast 
             if("generic".equals(Build.BRAND. toLowerCase())){ 
                 Toast toast = Toast.makeText(getApplicationCon text(),"ASR is not supported on virtual devices", Toast.LENGTH_SHORT); 
                toast.show(); 
                Log.d(LOGTAG, "ASR attempt on virtual device"); 
             } 
             else{ 
                setRecognitionParams(); //Read parameters from GUI 
                listen(); //Set up the recognizer and start listening 
             } 
       } 
});

当用户按下按钮时,将调用listen()方法,在其中设置RecognizerIntent的详细信息。在ASRWithIntent中,通过发送一个带有ACTION_RECOGNIZE_SPEECH动作的 Intent 来支持语音识别,使用startActivityForResult(Intent,int)方法,其中int值是由开发者定义的请求代码。如果它大于 0,当活动退出时,此代码将在onActivityResult()中返回。请求代码作为标识符,用于区分在应用程序中可能调用的不同意图产生的特定结果。

以下代码启动了一个识别语音的活动:

Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
// Specify language model
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
// Specify how many results to receive. Results listed in order of confidence
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, numberRecoResults);
// Start listening
startActivityForResult(intent, ASR_CODE);

正如代码所示,与ACTION_RECOGNIZE_SPEECH关联有几个额外的参数。其中一个是必需的,即EXTRA_LANGUAGE_MODEL。其他的是可选的。顾名思义,EXTRA_LANGUAGE_MODEL指定了在识别过程中要使用的语言模型。它支持以下两个选项:

  • LANGUAGE_MODEL_FREE_FORM:这个语言模型基于自由形式的语音识别,用于识别自由形式的语音,例如,在电子邮件的口述中。

  • LANGUAGE_MODEL_WEB_SEARCH:这个语言模型基于网络搜索词汇,用于模拟更为受限的输入形式,如较短的、类似搜索的短语,例如,飞往伦敦马德里的天气等等。

可以观察到,这两个选项都意味着相当不受限制的输入。要构建一个只接受特定关键词的应用程序,或者使用识别语法,目前需要处理识别结果,以匹配预期的模式。关于如何执行此操作的一些示例将在第六章,对话语法中展示。

其他额外的参数在 Android 文档的RecognizerIntent类中有描述。以下是一些更常用的可选额外参数:

  • EXTRA_PROMPT:这提供了一个文本提示,当要求用户说话时向用户展示。

  • EXTRA_MAX_RESULTS:这个整数值指定了返回结果的最大数量限制。如果省略,识别器将决定返回多少结果。这些结果是用户输入的不同可能的文本,并从最可能到最不可能排序(本章前面讨论的最佳 N 列表)。

  • EXTRA_LANGUAGE:这指定了可以代替设备上提供的默认语言的语言。其他语言的使用在第七章,多语言和多模态对话中进行介绍。

可以让用户从这些选项中选择,或者可以程序化地设置。ASRWithIntent应用会提示用户选择语言模型和最大结果数。如果没有提供这些信息,它会使用在两个常量中指示的默认值(请参阅asrwithintent.java文件中的showDefaultValuessetRecognitionParams方法)。

语音识别的结果会通过onActivityResults(int, int, Intent)中的活动结果返回。此时,识别已经完成。然而,通常我们会希望查看结果或以某种方式使用它们。为此需要执行的额外步骤在以下来自ASRWithIntent的注释代码中说明:

protected void onActivityResult(int requestCode, int resultCode, Intent data) { 
    if (requestCode == ASR_CODE) { 
        if (resultCode == RESULT_OK) { 
           //Retrieves the N-best list and the confidences from the ASR result 
           ArrayList<String> nBestList = 
             data.getStringArrayListE xtra(RecognizerIntent.EXTRA_RESULTS); 
           float[] nBestConfidences = 
             data.getFloatArrayExtra(RecognizerIntent.EXTRA_CONFIDENCE_SCORES;

           /** Creates a collection of strings, each one with a recognition result and its confidence, e.g. "Phrase matched (conf: 0.5)" */
           ArrayList<String> nBestView = new ArrayList<String>(); 

           for(int i=0; i<nBestList.size(); i++){ 
               if(nBestConfidences[i]<0) 
                 nBestView.add(nBestList.get(i) + " (no confidence value available)"); 
               else 
                 nBestView.add(nBestList.get(i) + " (conf: " + nBestConfidences[i] + ")"); 
           } 

           //Includes the collection in the listview of the GUI 
           setListView(nBestView); 

           //Adds information to log 
           Log.i(LOGTAG, "There were : "+ nBestView. size()+" recognition results"); 
       } 
       else { 
           //Reports error in log 
           Log.e(LOGTAG, "Recognition was not successful"); 
      } 
   } 
} 

如先前所讨论的,识别结果通过意图返回。可以使用getStringArrayListExtra方法,并传入RecognizerIntent.EXTRA_RESULTS作为参数来访问匹配的句子。同样,可以使用getFloatArrayExtraRecognizerIntent.EXTRA_CONFIDENCE_SCORES获取带有置信度分数的数组。

结果保存在一个新的ArrayList<String>中,其元素结合了匹配的字符串和浮点数组中的分数(表示为字符串)。请注意,有可能置信度分数是-1;这是置信度不可用时的情况。然后,在setListView方法中调用ArrayAdapter,将格式化的字符串插入到 GUI 的ListView中。

也可能发生识别无法满意进行的情况(在之前的代码中resultCode!=RESULT_OK)。在RecognizerIntent类中为主要的错误情况定义了不同的常量:RESULT_AUDIO_ERRORRESULT_CLIENT_ERRORRESULT_NETWORK_ERRORRESULT_SERVER_ERRORRESULT_NOMATCH。它们都对应于物理设备或网络的错误,最后一个对应于没有短语与音频输入匹配的情况。开发者可以使用结果代码对这些错误进行详细处理。然而,显示的对话框已经实现了一个简单的错误处理,对于较简单的应用来说可能已经足够。在这种情况下,用户会收到关于错误的反馈,并且在适用的情况下,他们会要求用户重复其话语。

最后,由于应用需要访问互联网以执行语音识别,因此需要在manifest文件中设置权限:

<uses-permission android:name="android.permission.INTERNET" />

ASRWithLib 应用

ASRWithLib应用与ASRWithIntent具有完全相同的功能,但不是只使用RecognizerIntent类,而是创建SpeechRecognizer类的实例。为了使代码在所有使用 ASR 的应用程序中可用,我们建议使用库,就像上一章中为 TTS 所做的那样。在这种情况下,库位于sandra.libs.asr.asrlib中,只包含一个类ASR,它实现了RecognitionListener接口,因此所有处理不同识别事件的方法,即onResultsonErroronBeginningOfSpeechonBufferReceivedonEndOfSpeechonEventonPartialResultsonReadyForSpeechonRmsChanged

SpeechRecognizer实例是在createRecognizer方法中创建的,在检查设备中是否可用 ASR 引擎之后:

List<ResolveInfo> intActivities = packManager.queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);

if (intActivities.size() != 0) {
    myASR = SpeechRecognizer.createSpeechRecognizer(ctx);
    myASR.setRecognitionListener(this);
}

这段代码查询PackageManager类以检查是否支持识别。如果intActivities.size()的值大于零,则支持语音识别,然后创建SpeechRecognizer的实例并将其保存在属性myASR中。识别的监听器通过引用this来设置,因为 ASR 类实现了RecognitionListener接口。

listen方法用于通过RecognizerIntent类开始监听。尽管额外的内容与ASRWithIntent应用中的相同,但启动识别的方式略有不同。ASRWithIntent使用了startActivityForResult,而在这个案例中,SpeechRecognizer对象负责通过接收意图作为参数来启动识别。

public void listen(String languageModel, int maxResults) throws Exception{ 

      if((languageModel.equals(RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) || languageModel.equals(RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH)) && (maxResults>=0){ 
          Intent intent = 
                new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 
          // Specify the calling package to identify the application 
          intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, ctx. getPackageName()); 

          // Specify language model	intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 
          languageModel); 

          // Specify how many results to receive. 
          // Results listed in order of confidence 
          intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults); 

          // Start recognition 
          myASR.startListening(intent); 
      }
      else 
          throw new Exception("Invalid parameters for listen method"); 
}

ASRWithIntent应用中,所有代码都在同一个类中,识别参数(languageModelmaxResults)的正确性测试也在那个单独的类中进行。然而,这里我们正在构建一个将在许多应用中使用的库,因此不能想当然地认为在调用listen方法之前会检查参数。这就是为什么该方法会检查它们的值,如果它们不正确,则会抛出异常。

有几种方法用来响应 ASR 引擎可能引发的不同事件。在库中实现这些事件的响应并不是一个好的策略,首先,这意味着所有使用该库的应用程序都必须对事件进行相同的处理;其次,因为大多数时候,对事件的响应涉及到向用户显示消息或以其他方式使用图形用户界面,而将应用程序的逻辑与其界面分离是一个基本的设计原则。

这就是为什么ASR类使用抽象方法的原因。抽象方法只声明头部,具体子类负责为它们提供代码。这样,负责响应 ASR 事件的每个方法都调用一个抽象方法,而 ASR 的每个子类中这些方法的行为都不同。在ASRWithLib示例中,ASRWithLib类是 ASR 的一个子类,并以特定方式实现抽象方法。如果我们有另一个应用程序,想要开发不同的行为,可以为这些方法编写单独的代码。

例如,当引擎找到与用户所说内容相匹配的句子时,会调用ASR.java类中的onResults方法。ASR.java中此方法的代码如下所示。请注意,为了获取结果,它使用了SpeechRecognizer类的静态方法,而不是像ASRWithIntent中那样使用RecognizerIntent

public void onResults(Bundle results) {
processAsrResults(results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION),results.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES));
}

此方法调用了抽象方法processAsrResults。在ASRWithLib类中实现了此方法,指出了如何处理结果(在这种情况下,是通过填充列表视图,如同ASRWithIntent中那样)。

如您可能已经观察到的,ASRWithLib应用程序没有显示识别对话框。这对于执行连续语音识别的应用程序(ASR 始终作为后台服务活动)可能是可取的,因为此类反馈可能会使用户感到烦恼。然而,对于其他应用程序,需要向用户显示一些反馈,以便他们知道应用程序正在听。这是通过onAsrReadyForSpeech方法完成的。当 ASR 引擎准备好开始听时,执行此方法,这是来自ASRLib的抽象方法,在ASRWithLib.java中通过更改语音按钮的颜色和信息来实现(文本和颜色都不是硬编码的,而是从res/values文件夹中获取的):

Button button = (Button) findViewById(R.id.speech_btn); button.setText(getResources().getString(R.string.speechbtn_listening));
button.setBackgroundColor(getResources().getColor(R.color.speechbtn_listening));

最后,需要在manifest文件中设置权限,以便使用 ASR 访问互联网和录制音频:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>

概括

本章展示了如何使用谷歌语音 API 来实现语音识别服务,前提是检查设备上是否提供该服务。系统会提示用户说出一些词语,识别结果,即识别出的字符串及其置信度分数,将显示在屏幕上。用户可以选择识别的语言模型以及要检索的最大结果数。这一功能是通过ASRWithIntentASRWithLib应用程序中的两种不同方法来实现的。

ASRWithIntent应用程序是一个简单易开发的基本示例,所有代码都包含在同一个类中。ASR 是通过使用RecognizerIntent类来完成的,并且有一个自动生成的对话框,提供反馈信息,指示引擎是否在听或是否出现错误。

ASRWithLib 应用程序展示了如何模块化并创建一个语音识别库,该库可以在许多应用程序中使用。它不是仅依赖于 RecognizerIntent 类,而是使用 SpeechRecognizer 类的实例,并使用抽象方法实现 RecognitionListener 接口,提供了一个灵活的实现,能够响应广泛的 ASR 事件。

虽然这两个示例并不是特别有用的应用程序,但这里展示的代码几乎可以原封不动地用在任何使用语音识别的应用程序中。本书后续的示例将基于此代码构建。下一章将展示如何结合 TTS(文本到语音)和 ASR(自动语音识别)来执行简单的语音交互,用户可以请求信息或向设备发出命令。

第四章:简单的语音交互

如果你可以仅通过向移动设备说话来获取信息或让它做事,那不是很好吗?本章介绍简单的语音交互,让你能够这样做。两个教程示例将向你展示如何实现查询信息以及请求启动设备上的一个应用程序。

语音识别并不完美,因此实施一些机制来选择最佳的识别结果很有意义。在前面的章节中,我们学习了如何获得置信度度量。在本章中,我们将介绍两种新机制:相似度度量,用于将识别的输入与用户所说的内容进行比较,以及确认机制,直接询问用户系统是否正确理解。

到本章结束时,你应该能够开发简单的语音交互,以请求信息并在设备上执行命令。你也应该了解如何使用相似度度量并确认用户所说的话。

语音交互

正如在第一章,安卓设备上的语音中讨论的,谷歌语音动作是简单的交互,用户说出一个问题或命令,应用程序通过动作或语音回应(或两者的组合)进行响应。

注意

以下是具有简单结构和少量交互回合的类似交互示例:

示例 1

用户:BBC 新闻

应用程序:(启动 BBC 新闻)

示例 2

应用程序:你的查询是什么?

用户:法国的首都是什么?

应用程序:(返回关于巴黎和法国的网页)

以下交互方式简单:

  • 有限的对话管理:交互最多由两到三个回合组成。

  • 有限的语音理解能力:用户仅限于由单个单词或短语组成的输入,例如网站名称或应用程序名称,或可以由谷歌搜索引擎处理的文本段落。

VoiceSearch 应用程序

这个应用程序说明了以下内容:

  1. 点击按下按钮说话选项时,提示用户说一些词语。

  2. 用户说出一些词语。

  3. VoiceSearch根据用户的语音发起搜索查询。

开屏界面有一个按钮,提示用户按下并说话。按下按钮后,下一界面显示谷歌语音提示你的查询是什么?结果会在浏览器窗口中显示。

在此案例中,应用程序使用了之前开发的两个库:TTSLib(见第二章,文本到语音合成)和ASRLib(见第三章,语音识别)。它们的jar文件包含在VoiceSearch项目的libs文件夹中。ASR 方法用于识别用户输入并将其用作搜索条件。TTS 用于向用户提供关于应用程序状态的语音反馈。

这个应用程序结合了TTSWithLib(第二章,语音合成)和ASRWithLib(第三章,语音识别)应用程序中已经呈现的代码。它使用与TTSWithLib中相同的 TTS 实例,以及ASRWithDemo中的相同 ASR 方法。它还通过改变按钮的颜色和文字,提供视觉反馈以指示应用程序是否正在监听用户。

一旦 ASR 产生结果,它将被用于执行谷歌搜索,使用一个网页搜索意图,如processAsrResults方法所示:

public void processAsrResults(ArrayList<String> nBestList, float[] nBestConfidences) {
      String bestResult = nBestList.get(0); 
      indicateSearch(bestResult); //Provides feedback to the user that search 
      //is going to be started 
      //Carries out a web search with the words recognized 
      Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); 
      intent.putExtra(SearchManager.QUERY, bestResult); 
      startActivity(intent); 
}

可以观察到,应用程序总是使用最佳结果启动网络搜索。然而,系统可能误解了用户说的话,在这种情况下,启动的搜索可能是错误的。为了避免这种情况,可以使用确认。本章末尾将介绍确认的引入以及使用VoiceSearch应用程序的一个简单示例。

VoiceLaunch 应用程序

这个应用程序的功能如下:

  1. 当用户点击按下说话按钮时,系统会提示用户说出一个应用程序的名称。

  2. 用户说出一个应用程序的名字。

  3. VoiceLaunch会将识别的输入与设备上安装的所有应用程序的名称进行比较,并启动名称最相似的应用程序。

像 VoiceLaunch 这样的应用程序不需要任何界面,因为用户只需说出他们想要启动的应用程序名称即可。然而,为了说明起见,我们创建了一个简单的界面,用户可以在其中选择两个参数的值:一个相似度阈值和一个相似度准则,如下面的截图所示。截图显示了用户要求启动邮件的场景。VoiceLaunch显示了图中的屏幕并启动了邮件应用程序(在这种情况下是相似度最高的,为1.00):

VoiceLaunch 应用程序

我们介绍了相似性准则的技术,以展示如何改善 ASR 的结果。例如,当你有一个需要识别的关键词列表,但你不能限制 ASR 只监听这些词时,识别结果可能并不完全等同于你所期望的关键词。比如,如果你的关键词是冰淇淋口味,你可能考虑巧克力草莓香草,但如果你的 ASR 没有限制,对草莓的识别结果可能是蔓越莓。如果你的应用程序期待一个完全匹配,它会丢弃识别结果。然而,有了相似性度量,你的应用程序会知道用户说了与草莓相似的内容。

VoiceLaunch中,相似度标准用于控制应用如何测量识别的名称与设备中安装的应用名称之间的相似度。使用相似度阈值,以便如果应用的名称与用户输入的识别内容不够相似,则不会启动该应用。

我们考虑以下两种计算相似度的选项:

  • 正字法相似度VoiceLaunch计算单词之间的 Levenshtein 距离。这个度量标准将单词 A 和 B 之间的距离视为在 A 中执行插入、删除或替换以获得 B 所需的最小字符数。VoiceLaunch将距离值转换为 0 到 1 之间的相似度值。

  • 语音相似度VoiceLaunch使用Soundex算法来计算名称之间的语音相似度。所使用的实现仅对英语有效,这就是为什么VoiceLaunch应用仅提供英语版本。这样,即使拼写不同,发音相同的单词也会被认为是相似的。相似度也使用从 0 到 1 的区间进行测量。

备注

考虑的距离度量有很多:欧几里得距离、杰卡德指数、汉明距离、索伦森相似度指数或梅特 afone。

通常同音词的拼写相似,因此正字法和语音相似度的值几乎相同,例如,totoo,或flowerflour。可以观察到,当处理 ASR 结果时,语音相似度更为方便,而当处理文本结果时,正字法相似度更好,因为文本中拼写错误更常见。以下表格显示了一些示例:

相似度标准 示例单词对
Addition – Edition
正字法相似度 0.75
语音相似度 0.75

备注

一本配套的小型 Java 项目可在书籍网页上找到,你可以使用ComparisonTest java 项目来创建自己的测试。为了使用它,你必须包含 Soundex 的 apache 库,并将要比较的单词作为运行参数。在 Eclipse 中包含库,请右键点击项目 | 属性 | | 添加外部 JARs。要运行项目,请右键点击运行方式 | 运行配置,在程序参数标签下,用空格分隔两个单词(例如,to too)。

处理 ASR 和 TTS 的代码与 VoiceSearch 应用相似。然而,在识别完成后,VoiceLaunch 必须找到与用户识别词相似的应 用,并启动最相似的一个。这通过在 processAsrResults 方法中完成(在代码包的 VoiceLaunch.java 文件中查看),该方法调用 getSimilarAppsSortedlaunchApp 方法,这些方法将在以下页面中描述。

提示

你是否注意到…

所有应用中使用的语音识别(ASR)库使它们具有相似的结构,其中在 processAsrResults 方法中处理识别信息。例如,在 VoiceSearch 应用中,它使用识别条件开始搜索,而在 VoiceLaunch 中,它获取已安装的类似应用列表并启动最相似的一个。

此外,VoiceLaunch 必须存储和管理关于设备上可用应用的信息。为了高效地完成这项工作同时保持代码的可读性,我们创建了一个名为 MyApp 的辅助类(你可以在代码包中找到,位于 VoiceLaunch 项目的 VoiceLaunch.java 文件中),对于每个应用,我们保存其用户友好的名称(例如,Adobe Reader),其被 Android 可识别的包名(例如,com.adobe.reader),以及其与用户输入的相似度(从 0 到 1 的值,例如,0.7)。

由于需要排序已安装的应用,我们还为 MyApp 类的对象定义了一个比较器。比较器如果第一个元素小于第二个元素返回负数,如果它们相等返回 0,如果第一个元素大于第二个元素返回正数。然后它们可以用来将集合从最小值排序到最大值。在我们的案例中,我们希望从最高相似度到最低相似度排序 MyApps 的集合。因此,我们将相似度值的比较器结果乘以-1,以得到反向排序的结果:

private class AppComparator implements Comparator<MyApp>{ 
    @Override 
    public int compare(MyApp app1, MyApp app2) { 
      return (- Double.compare(app1.getSimilarity(), app2\. getSimilarity())); // Multiply by -1 to get reverse ordering (from most to least similar) 
    } 
}

如前所述,VoiceLaunch 使用 getSimilarAppsSorted 方法来获取设备上按相似度排序的应用列表,但它只考虑那些与识别名称相似度高于指定阈值的应用。

如代码包所示(在 VoiceLaunch 项目下的 VoiceLaunch.java 文件中),该过程通过以下步骤进行:

  1. 从包管理器中检索所有已安装应用的列表。

  2. 对于列表中的每个应用,使用用户在 GUI 中选择的算法计算其与识别名称的相似度。如果获得的相似度值高于阈值,通过创建 MyApp 类的实例来保存应用的名称、包名和相似度值,该实例被添加到 similarApps 集合中。

  3. similarApps集合是使用我们的相似度比较器进行排序的:Collections.sort(similarApps, new AppComparator());

  4. 这些信息被保存在日志中,以便开发者了解在相似度方面考虑了哪些应用。

    public void processAsrResults(ArrayList<String> nBestList, float[] nBestConfidences) {
    
          //Obtains the best recognition result 
          String bestResult = nBestList.get(0); 
    
          //Read the values for the similarity parameters from the GUI 
          readGUIParameters(); 
          //Obtains the apps installed in the device sorted from most to least similar name regarding the user input 
    
          //String[] = [0] = name, [1] = package, [2] = similarity 
          ArrayList<MyApp> sortedApps = getSimilarAppsSorted(bestRes ult); 
    
          //Shows the matching apps and their similarity values in a list 
          showMatchingNames(sortedApps); 
    
          //Launches the best matching app (if there is any) 
          if(sortedApps.size()<=0) 
          { 
              Toast toast = Toast.makeText(getApplicationContex t(),"No app found with sufficiently similar name", Toast.LENGTH_ SHORT); 
              toast.show(); 
              Log.e(LOGTAG, "No app has a name with similarity > 
              "+similarityThreshold); 
          }
          else 
              launchApp(sortedApps.get(0));
    }
    

提示

你也可以尝试…

可以使用另一种方法,使系统不仅仅选择一个相似度度量,而是综合考虑两种方法的结果来选择最相似的应用,例如,使用加权投票方法。

根据所使用的相似度算法,getSimilarAppsSorted调用以下方法之一:compareOrthographiccomparePhonetic(见代码捆绑包中VoiceLaunch项目下的VoiceLaunch.java)。前者使用 Levenshtein 度量计算正字法距离,后者使用 Soundex 算法计算语音距离。

为了计算 Levenshtein 距离,我们使用了com.voicedemos包中的LevenshteinDistance.java类。它基于维基教科书提供的代码,该代码可以在en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java找到,我们在此基础上添加了将距离转换为 0 到 1 之间相似度值的代码。

为了计算语音距离,我们使用了 Apache Commons 提供的 Soundex 实现,可在commons.apache.org/proper/commons-codec/index.html找到。为此,我们将对应的commons-codec-1.8 libjar文件添加到了VoiceLaunch项目的libs文件夹中。

这两个相似度计算方法的输入参数是两个字符串,分别对应考虑中的应用名称和识别的输入。这些字符串之前通过删除空格并使用小写进行了标准化处理:

    private String normalize(String text){
        return text.trim().toLowerCase();
    }

注意事项

可以进行更复杂的标准化处理,以应对用户只说了一个双词名称中的一个词的情况,例如,说kindle而不是kindle reader

应用排序后,最相似的应用通过使用launchApp方法启动,该方法使用带有应用包名的launchintent(你可以在代码捆绑包中的VoiceLaunch.java文件中找到它)。

VoiceSearchConfirmation 应用

确认是事务性对话中非常重要的一个方面,人类在服务交易中也广泛使用以确信一切都已被正确理解。由于当前的语音识别技术不能保证应用确切听到了用户所说的内容,应用应该确认用户的意图,尤其是如果下一个动作可能导致不可恢复的后果时。然而,确认应该谨慎使用,因为它们会延长交互时间,如果过度使用可能会让用户感到烦恼。

VoiceSearchConfirmation应用程序与VoiceSearch功能相同,但在执行搜索之前会确认搜索条件。与该应用程序的两个示例交互如下:

  • 确认情景:此情景的特点如下:

    1. 用户按下按钮说话,并说出了贝尔法斯特的天气

    2. 系统理解了贝尔法斯特的天气并询问你是说贝尔法斯特的天气吗?

    3. 用户按下按钮说话,并说是的

    4. 系统使用贝尔法斯特的天气作为条件启动搜索。

  • 否定情景:其特点如下:

    1. 用户按下按钮说话,并说格拉纳达的天气

    2. 系统理解了加拿大的天气并询问你是说加拿大的天气吗?

    3. 用户按下按钮说话,并说

    4. 对话返回到步骤 1,直到用户满意为止。

processResults方法在 ASR 识别出用户输入的任何内容时都会被调用。因此,为了给应用程序提供确认功能,需要区分方法是识别搜索条件后调用,还是识别确认请求的“是/否”响应。为此,我们引入了新的属性searchCriterion来存储已识别的条件。如果为空,我们会尝试识别新的条件,如果不是,则确认其值。您可以查看VoiceSearchConfirmation.java文件中的代码,该文件位于VoiceSearchConfirmation项目的代码包中。

提示

其他相似度衡量方法和技术

其他相似度衡量方法和用于增强谷歌语音识别器返回结果的技巧在 Greg Milette 和 Adam Stroud 所著的《Professional Android™ Sensor Programming》第十七章中有所描述。讨论的技巧包括:

使用词干提取来提高词汇识别,即通过去掉词尾来将单词简化为词根,例如,将 walk、walks、walked 和 walking 都简化为同一个词根。

音韵索引,即匹配在发音上相似的词汇,例如,如果识别器返回发音相似的词appeal,能够返回apple

使用 Lucene 进行匹配,这是一个为文本搜索而设计的搜索引擎库。

总结

在本章中,我们展示了如何使用谷歌语音识别和 TTS API 开发简单的语音交互。第一个示例说明了如何从用户那里获取一些词语作为输入并启动搜索查询。第二个示例涉及使用语音在设备上启动应用程序。这里我们介绍了使用相似度度量来比较用户输入的识别结果与可能说出内容的技术。文中展示了两种不同的度量方法:正字法相似性和音韵相似性。最后一个示例展示了如何使用确认来检查系统是否正确识别了输入。这些技术,加上前一章引入的置信度分数,对于开发支持语音的应用程序非常有用。

然而,这些交互在两个方面受到限制。首先,它们没有涉及使用对话状态信息来控制交互,以及确定应用程序应该说什么和做什么。应用程序的行为在特定的语音动作中是硬编码的。其次,这些交互限制了用户只能输入一个简单单词或短句。更复杂的对话需要对话状态的表示以及更高级的口语语言理解。第五章展示了如何包含对话状态的表示,以提供更灵活的对话管理,而第六章则展示了如何使用语法来允许更高级的口语语言理解。

就确认而言,我们针对只需确认单个数据的情况提出了一种非常简单的解决方案。在接下来的章节中,我们将研究如何创建更为复杂的行为,以便可以确认多个数据,同时考虑识别置信度的确认策略。我们还将介绍一种无需按下按钮即可进行对话的方法。

第五章:表单填写对话

许多启用语音的应用程序使用前一章描述的一键式对话。你觉得语音界面可以比这走得更远吗?你能想象更复杂的交互,其中需要从用户那里获取多个信息项,用于各种目的,例如启动应用程序、查询数据库、启动网络服务或网络服务混搭,以及更多?

这类对话与传统的网络应用程序中的表单填写相似。在本章结束时,你应该能够实现简单的表单填写对话,以便获取访问网络服务所需的数据。

表单填写对话

表单填写对话可以看作是多个待填写的槽位。例如,在航班预订应用程序的情况下,系统可能需要填写五个槽位:目的地、到达日期、到达时间、出发日期和出发时间。在简单的表单填写对话中,每个槽位逐一处理,并提出相关问题,直到所有槽位都被填写。此时,应用程序可以查找所需的航班,并将结果呈现给用户。以下是一个对话可能进行的方式的示例,以及对话进行时槽位状态的变化。

应用程序:欢迎来到航班信息服务。你想要去哪里旅行?

通话者:伦敦。

槽位 目的地 到达日期 到达时间 出发日期 出发时间
--- --- --- --- --- ---
伦敦 未知 未知 未知 未知

应用程序:你想要哪一天飞往伦敦?

通话者:7 月 10 日。

槽位 目的地 到达日期 到达时间 出发日期 出发时间
--- --- --- --- --- ---
伦敦 07/10/2013 未知 未知 未知

框架的槽位也可以附加条件,例如,如果是往返旅程,那么还需要填写返回旅程的槽位值。

在更复杂的对话中,用户可以一次填写多个槽位,如下面的示例所示:

应用程序:欢迎来到航班信息服务。你想要去哪里旅行?

通话者:我想在周五飞往伦敦。

槽位 目的地 到达日期 到达时间 出发日期 出发时间
--- --- --- --- --- ---
伦敦 21/06/2013 未知 未知 未知

由于用户在回答目的地问题时已经提供了到达日期,因此可以填写该槽位,系统可以跳过到达时间的问题。然而,为了处理本例中通话者的输入,需要更复杂的语法,如第六章中讨论的对话语法

实现表单填写对话

为了实现表单填写对话,需要:

  • 创建一个数据结构来表示系统必须从用户那里获取信息的槽位。

  • 开发一个算法来处理槽位,提取每个槽位所需的提示语。

VoiceXML(www.w3.org/TR/voicexml20/)为这项任务提供了一个有用的结构,即包含表示完成表单所需不同信息项(槽位)的表单字段。以下代码是一个示例:

  <form id = "flight">
  <field name="destination">
  <prompt>where would you like to travel to?</prompt>
  <grammar src = "destinations.grxml"/>
  </field>
  <field name="date">
  <prompt>what day would you like to travel?</prompt>
  <grammar src = "days.grxml"/>
  </field>
  </form>

前面的示例展示了一个应用,该应用需要获取两项信息:目的地和日期。为了询问目的地,它使用了提示语 where would you like to travel to?,而询问日期时,它合成了 what day would you like to travel?。每一条数据都是通过重复提示用户直到收集完所有信息的方式来顺序获取的。在 VoiceXML 中,这是通过使用表单解释算法FIA)完成的,具体描述见www.w3.org/TR/voicexml20/#dml2.1.6

尽管在本书中完全实现 VoiceXML 表单填充的方法超出了范围,但在本章的剩余部分,我们将展示如何创建一个简单的表单填充应用,该应用使用了类似的数据结构和算法。

提示

你也可以尝试…

你可以使用Voxeo Evolutionevolution.voxeo.com/)来创建自己的 VoiceXML 对话。你可以注册一个免费的开发者账户,然后你就可以创建自己的 VoiceXML 应用,并将分配一个 Skype 号码,你可以使用它与应用互动。请注意,这对于 Android 应用来说并不直接有用,但可以让你了解 VoiceXML 以及它作为对话脚本语言是如何工作的。

你可能还想研究使用 JVoiceXML(jvoicexml.sourceforge.net/)来构建一个完整的 Java VoiceXML 解析器。针对 Android 的初步实现代码可以在这里找到:sourceforge.net/p/jvoicexml/code/HEAD/tree/branches/android/

线程处理

在剩余章节中,我们将出于各种目的使用 XML 文件,并将公共代码封装在XMLLib库中。一个重要的问题涉及到线程处理。启动应用时,会创建一个线程来运行代码。这个线程负责涉及更新用户界面的操作,因此有时被称为UI 线程。在 UI 线程中执行非常耗时的操作,如下载文件、执行 HTTP 请求、打开套接字连接或访问数据库,可能会使 UI 线程长时间阻塞,导致界面无响应并冻结更新。因此,从 Android 3(HoneyComb)开始,当尝试在 Android 应用的 主线程上执行网络操作时,会引发android.os.NetworkOnMainThreadException异常。

Android 提供了多种方法来实现后台线程与 UI 线程之间的通信,具体解释请参见:developer.android.com/guide/components/processes-and-threads.html

其中一种方法是使用异步任务(AsyncTask)。AsyncTask始终在后台运行耗时的操作,并在 UI 线程中发布结果。关于AsyncTask的文档可以在这里找到:developer.android.com/reference/android/os/AsyncTask.html

简而言之,当执行异步任务时,它会经历四个阶段,可以通过以下方法进行控制:onPreExecute在任务执行之前,doInBackgroundonPreExecute之后立即执行后台计算,onProgressUpdate显示操作进度(例如,在用户界面中的进度条),以及onPostExecute,当后台计算完成时调用。

XMLLib

在我们的库中(代码包中的sandra.libs.util.xmllib),RetrieveXMLTask(见RetrieveXMLTask.java)负责从网上获取 XML 文件并将其内容保存为字符串以供进一步处理。它是以下列方式声明的:

  class RetrieveXMLTask extends AsyncTask<String, Void, String>

它被定义为一个异步任务(AsyncTask),接收一个字符串集合作为输入参数。它不产生任何类型的进度值(void),并以字符串形式产生后台计算的结果(<parameters, progress, result>)。在我们的案例中,字符串输入是获取 XML 文件的 URL,字符串结果是文件中的 XML 代码。从指定 URL 读取 XML 文件作为doInBackground方法中的后台任务,该方法使用其他私有方法来打开 HTTP 连接和读取字节流(saveXmlInStringreadStream)。查看代码包中的doInBackgroundsaveXMLInString方法(XMLLib项目,RetrieveXMLTask.java)。这里有一个关于如何进行异步 HTTP 请求的好教程:mobiledevtuts.com/android/android-http-with-asynctask-example/

可以观察到,RetrieveXMLTask中的异常处理以一种特殊的方式进行,因为有一个名为exception的属性,用于保存连接或读取操作(doInBackground方法)期间可能引发的异常。一旦异步任务完成(在onPostExecute方法中),就会处理此类异常。

一旦后台任务完成,我们需要使用onPostExecute方法将结果返回给调用类。为此,我们定义了接口XML AsyncResponse(见XML AsyncResponse.java),其中包含抽象方法processXMLContents。这个想法是调用异步任务的类实现该接口,并提供processXMLContents方法的代码,而onPostExecute将输出处理委托给此方法(见代码包中的onPostExecute方法:sandra.libs.util.xmllib.RetrieveXMLTask)。

表单填充库

要构建一个表单填充应用程序,我们必须指定一个类似于飞行示例中的数据结构。为此,我们定义了两个类:FormField。如 UML 图所示,Form拥有字段集合,而Field具有五个属性:一个名称,一个表示应用程序将用于请求该数据片段的提示字符串,以及两个表示当应用程序不理解用户对初始提示的响应(nomatch)或听不到(noinput)时使用的提示字符串,以及应用程序理解的值。

FormFillLib

例如,Field飞行设置可能为其属性具有以下值:

name: 目的地

prompt: 你的目的地是哪里?

nomatch: 抱歉,我没理解你说的话

noinput: 抱歉,我听不到你的声音

value: 罗马(当用户对系统提示回答罗马时)

这种结构足以构建我们本章讨论类型的程序。只需创建与待填充槽位数量相等的Field类对象,以及一个包含所需字段集合的Form。这可以通过 Java 编程轻松实现,然而它对读者不太友好,程序员也很难更改一些如系统提示之类的参数。

为解决此问题,我们将实现 VoiceXML 标准的一个子集,以创建易于阅读的 XML 文件,其中包含对话的结构。然后,我们将使用VXMLParser类自动解析 XML 文件,并将它们转换为FieldForm类的对象。最后,DialogInterpretation类将使用这些对象来管理交互。

注意

这并不意味着 VoiceXML 设计完全独立于应用程序。应用程序需要知道哪些信息需要处理,这些信息在 VoiceXML 文件中作为字段指出。因此,开发者可以更改 VoiceXML 中的任何内容,除了字段的数量和名称。

VXML 解析器

在 Android 中实现解析器有不同的方法,主要是使用 DOM、SAX 或 Pull 解析器。Android 推荐使用XMLPullParser类(developer.android.com/reference/org/xmlpull/v1/XmlPullParser.html),因为它设计得更为高效和简单。XML 的 Pull 解析允许流式解析,处理过程可以随时中断和恢复。

为了解析 VXML 文件,我们创建了一个XMLPullParser来读取 XML 内容,并不断调用next()方法来获取下一个事件,直到事件为END_DOCUMENT。这样,我们处理START_TAGTEXTEND_TAG事件,分别对应系统遇到开始标签、处理标签内的文本或遇到结束标签的情况。

当系统遇到

标签时,我们会创建一个新的Form对象。由于解析器的理念是处理单个表单,如果文件中有多个表单,我们只跟踪最后一个。这是通过将Form类的实例作为VXMLParser类的一个属性来实现的。

对于字段,它们的名称是通过解析标签的属性提取的。然后,当遇到相应的 VoiceXML 标签时,我们也会保存关于promptnomatchnoinput提示的信息。

任何使用 VoiceXML 解析器的类都可以调用parseVXML方法(见sandra.libs.dm.fomfilllib.VXMLParser),以获取解析数据时创建的表单。解析器使用FormFillLibException类,以便对异常进行更细粒度的控制。

DialogInterpreter

DialogInterpreter类似于 VoiceXML 中的 FIA 算法。它按顺序访问所有字段,直到应用程序为每个字段获取语音识别结果。在每个字段中,它合成提示并监听用户输入。如果语音识别成功,则移动到下一个字段;如果失败,系统将重复当前字段的过程。以下伪代码描述了解释器的行为:

Interpret field i:
        Play prompt of field i
        Listen for ASR result
        Process ASR result:
            If the recognition was successful, then save recognized keyword as value for the field i and move to the next field
            If there was a no match or no input, then interpret field i
            If there is any other error, then stop interpreting

Move to the next field:
        If the next field has already a value assigned, then move to the next one
        If the last field in the form is reached, then endOfDialogue=true

伪代码中描述的行为在DialogInterpreter类中开发。这个类使用不同的属性来控制解释:必须解释的Form、表示字段在表单字段列表中位置的整型currentPosition、两个用于nomatchnoinput事件的提示字符串(如果 VoiceXML 文件中有可用的,则会被解析出来的标签替换),以及包含解释结果的HashMap,形式为字符串对<字段名, 字段值>。

当调用 startInterpreting 方法时开始解释。要解释的表单被提供,currentPosition 初始化为 0,并调用 interpretCurrentFieldinterpretCurrentField 方法没有封装 interpret field i 伪代码中显示的所有行为,因为需要使用不同的方法来控制各种 ASR 事件。因此,interpretCurrentField 只控制第一部分:播放字段提示并启动 ASR。然后,使用 ASRLib 中的 processAsrResultsprocessAsrErrors 方法来处理不同的事件。前者控制 ASR 从用户输入中理解到内容的情况,将其值保存在结果 Hashmap 中,并调用 moveToNextField 方法。

后者控制 ASR 出错的情况,如果错误代码是 ERROR_NO_MATCHERROR_SPEECH_TIMEOUTnomatchnoinput),将合成相应的提示并重新解释字段。如果错误是由于网络问题,则停止解释。你可以在代码包中的 sandra.libs.dm.formfilllib.DialogInterpreter 查看这个方法。

moveToNextField 方法与之前显示的伪代码类似:当字段没有值时,它会向前移动当前字段的位置。如果存在没有值的字段,它将被解释;如果没有更多字段需要解释,则解释完成并调用 processDialogResults 方法。

processDialogResults 方法是抽象的,这样任何 DialogInterpreter 的子类都可以提供自己的代码来处理解释的结果。在下一节中,我们将展示一个使用 FormFillLib 库并实现此方法的应用程序,以使用对话结果调用网络服务。

提示

你也可以尝试…

另一个有趣的选择是在 Java 代码中用不同的数据结构编写对话的结构。这是 Deitel, P., Deitel, H., Deitel, A. 和 Morgano, M. 在其著作《Android for Programmers: An App-Driven Approach》(Prentice Hall 2011)第十五章中的“Pizza Ordering App”示例所采用的方法,你可以在这里找到:ptgmedia.pearsoncmg.com/imprint_downloads/informit/bookreg/9780132121361/android_15_speech_final_a.pdf

我们所采用的方法的优势在于,我们提供了不同的库来解析和管理交互,这些库可以被任何需要与基本表单填充对话框接口的应用重复使用。因此,这是一种通用方法,每个特定应用程序的详细信息都在一个简单的 VoiceXML 文件中提供。下一节将展示一个示例。

MusicBrain 应用

为了说明如何使用FormFillLib,我们将开发一个应用程序,要求用户提供查询网络服务所需的数据片段。本节中描述的应用程序中的类与库之间的关系在以下类图中展示:

MusicBrain 应用

应用程序不再是独立的孤立应用;它们通常将自身的资源与从第三方网络服务收集的数据和功能相结合。最近,许多网络应用程序发布了 API(应用程序编程接口),允许感兴趣的开发商在自己的应用中使用它们。这种集成可以尽可能复杂,涉及多个来源。这些被称为 mashups。例如,一个旅行 mashup 可以整合谷歌地图来标示地理位置,同时用 Flickr 显示相关旅游景点的图片,并在 FoodSpotting 中查找好的餐厅。

可以在以下网址找到一系列可用的 API 列表:www.programmableweb.com。其中一些(例如,亚马逊、谷歌和脸书)要求您注册才能使用它们的 API。其他只能由授权合作伙伴使用,还有一些是公开的,可供公众(通常是非盈利)使用。

对于MusicBrain应用(sandra.examples.formfill.musicbrain),我们将使用MusicBrainZ公共 API。MusicBrainZ(musicbrainz.org/)是一个开放的音乐百科全书,包含音乐的元数据。它允许用户查询地区(特定地点的音乐)、艺术家或团体、专辑和发行版。

MusicBrain 应用提供了关于在两个日期之间发布的标题包含特定文本的专辑的信息。与应用程序的一个示例交互如下所示:

应用程序> 请说出专辑标题中的一个词

用户> robot

应用程序> 请说出搜索的起始日期

用户 > 1970

应用程序> 请说出搜索的最终日期

用户> 2000

前一交互的结果产生了如下截图所示的结果:

MusicBrain 应用

表单填充对话框在MusicBrain.java中创建活动时启动。这涉及到获取 VoiceXML 文件,解析它,并解释它以获取三个数据片段(标题中的词、起始年份和最终年份),这些数据将用于查询 MusicBrainZ 网络服务。VoiceXML 代码如下:

  <form>
  <field name="query">
  <prompt>Please say a word in the album title</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said </nomatch>
  </field>
  <field name="initialyear">
  <prompt>Please say the starting year for the search</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said </nomatch>
  </field>
  <field name="finalyear">
  <prompt>Please say the final year for the search</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said
  </nomatch>
  </field>
  </form>

对话结束后,应用程序查询 MusicBrainZ,并获得一个包含用户说出的词的所有专辑的 XML,然后解析并筛选 XML,只保留在选定日期之间发布的专辑。最后,在 GUI 中向用户展示信息。

注意

网络服务通常以 XML 或 JSON 格式提供结果。我们使用 MusicBrainZ 的 XML 版本,因为在这个特定的 API 中它更稳定,尽管在使用其他网络服务时,您可能需要考虑其他选项。

语音对话由startDialogprocessXMLContentsprocessDialogResults方法控制。startDialog方法初始化 ASR 和 TTS 引擎(使用前几章描述的ASRLibTTSLib库)并开始检索 VoiceXML 文件。如前所述,当检索到 VoiceXML 文件时,会调用processXMLContents方法。由于应用程序中可能检索到两种 XML 文件(包含对话的 VoiceXML 和来自网络服务的结果的 XML),processXMLContents方法会检查当前正在处理的 XML 类型,并启动相应的解析过程。最后,当 VoiceXML 解析完成后,将调用processDialogResults方法。此方法使用用户在对话中提到的专辑标题中的单词查询 MusicBrainZ。请查看代码包中的startDialogprocessDialogResult方法(MusicBrain.javaMusicBrain项目)。

对网络服务的查询结果编码在一个 XML 文件中,其结构在musicbrainz.org/doc/Development/XML_Web_Service/Version_2/Search#Release中解释。这个文件在parseMusicResults方法中被解析。该方法使用了MusicBrainParser类(见MusicBrainParser.java),在其中我们实现了一个XMLPullParser。解析器遍历 XML 文件,获取一系列专辑。为此,我们定义了Album类,其中包含有关每张专辑的标题、表演者、发行日期、国家以及唱片公司的信息。

我们使用警告对话框来显示解析、ASR 或 TTS 错误的原因。这样,用户可以更好地意识到应用程序不工作是因为格式错误的 XML 或互联网连接问题。创建这些对话框的方法是createAlert(在MusicBrain.java中)。

一旦 XML 文件被解析成一系列的Album对象,这些对象将通过filterAlbums方法(MusicBrain.java)进行过滤。这样,从所有标题包含所需单词的专辑中,我们只考虑那些在用户提供的日期之间发行的专辑。如果识别结果无法解析为日期,那么所有专辑都将被考虑在内。

筛选专辑后,我们使用showResults方法在 GUI 上显示它们。不必在事后对专辑进行排序,因为我们将它们保存在一个TreeSet中,这是一个允许我们避免重复并按顺序保存元素的集合。为此,我们在Album.java中重写equals方法,使用代码比较两个专辑是否相等(我们检查它们是否有相同的标题和解释者),并在AlbumComparator.java中创建一个自定义比较器,根据专辑的发行日期进行比较,以便可以从最近到最远排序。

概述

本章展示了如何实现表单填充对话,应用程序通过与用户的简单对话来获取多块数据,这些数据稍后可以通过网络服务或混搭为用户提供高级功能。

FormFillLib包含了用于检索和解析对话结构的 XML 定义到 Java 对象的类。这些对象用于控制与用户的口头交互。这个库使得通过在互联网上访问简化的 VoiceXML 文件来轻松构建 Android 应用程序中的任何表单填充对话成为可能。

MusicBrain应用程序展示了如何通过语音对话从用户那里收集信息,以查询一个网络服务。在这个例子中,应用程序询问用户一个单词和两个日期,用于在 MusicBrainZ 开放音乐百科全书查询在这些日期之间发布且标题中包含该单词的专辑。该示例还展示了如何解析和过滤网络服务提供的结果,以便向用户展示。

在此示例中,ASR 输入没有限制,但在许多应用程序中,限制用户可能使用的词汇或短语是可取的。在第六章《对话语法》中,我们将研究如何使用语法来限制输入,同时允许相当复杂的结构,并添加语义以提供输入的解释。

第六章:对话语法

你可能已经注意到,在前一章中研究的表单填充对话中的输入被限制为单个单词和短语。本章介绍使用语法来解释更复杂的输入,并提取它们的意义。在商业应用中常用的两种语法是:对于可预测和定义良好的输入,使用手工制作语法;对于典型的对话语音中不太规则的输入,使用统计语法以获得更稳健的性能。

在本章结束时,你应该能够开发支持更广泛用户输入的应用程序,利用手工制作以及统计语法。

用于语音识别和自然语言理解的语法

语法在基于语音的应用程序中可以用于两个不同的目的,如下所示:

  • 语音识别:在这种情况下,语法(也称为语言模型)指定识别器可以预期的单词和短语。例如,如果系统处理的是城市,它就不应该尝试识别数字。根据 W3C 定义的语音识别语法,可参考www.w3.org/TR/speech-grammar/,可以由开发者明确指定(手工制作语法),也可以从语言数据计算得出(统计语法)。语音识别语法有助于提高语音识别的准确性。

  • 自然语言理解:这个想法是获取识别器的输出,并为单词分配语义解释(或意义)。这可以通过几种方式完成。一种方法是确定句子的结构(句法分析),然后为这个结构分配语义解释(或意义)。或者,也可以不经过句法分析阶段,直接从句子中提取语义解释。

目前,谷歌语音识别 API 不支持开发者指定的语法用于语音识别,唯一可用的语言模型是内置的LANGUAGE_MODEL_FREE_FORMLANGUAGE_MODEL_WEB_SEARCH。在本章将要介绍的应用程序中,我们将像前几章一样进行语音识别,然后过滤识别结果,只保留符合语法的那些结果,并使用它们来获取用户输入的语义解释。

提示

你也可以尝试…

尽管目前在 Android 的RecognizerIntent中还不能使用语法,但可以使用第三方选项。访问我们的网页以获取更多详细信息。

使用手工制作语法的 NLU

设计语法需要预测用户可能说的不同内容,并创建规则来涵盖这些内容。语法设计是一个迭代过程,包括创建初始语法、收集数据以测试语法对实际用户输入的适应性、添加一些短语和删除其他短语,等等,直到语法的覆盖面尽可能完整。有许多工具可以帮助设计语法。例如,Nuance 提供了 Nuance 语法构建器,可以用来测试语法的覆盖范围,检查测试短语是否得到正确的语义解释,以及测试过度生成,即检测输入中不必要的或意外的短语(evolution.voxeo.com/library/grammar/grammar-gsl.pdf)。

有不同的语言用于指定语音语法,最流行的是由 W3C 定义的 XML 和增强 BNFABNF),可在www.w3.org/TR/speech-grammar/获取,Java Script 语法格式JSGF),被 Java 语音 API 使用,以及语法规范语言GSL),这是 Nuance 专有的格式。

在本章中,我们将使用 XML 格式,并介绍一个能够解析简单 XML 语法以用于类似于前一章研究的表单填充对话的库。开发完整的 XML 语法处理器超出了本书的范围,但关于它应该具有的功能的更多信息可以在 W3C 语音识别语法规范中找到,该规范可在www.w3.org/TR/speech-grammar/#S5获取。

下面的简单语法使用了一些来自 XML 语法格式的标签。这个语法可以识别句子show flights to London

<grammar root = "flight_query">

<rule id = "flight_query"> 
    <item>show</item> 
    <item>flights</item> 
    <ruleref uri = "destination" />
</rule>

<rule id = "destination"> 
    <item> to London </item> 
</rule>

</grammar>

如所见,语法是按规则组织的。<grammar>标签指明了语法的起始元素(或根),标记为flight_query<grammar>标签后的第一个规则必须与根元素同名。这个规则包含两项:单词show和单词flights,涵盖了要处理字符串的前两个词。第三部分是对另一个名为destination的规则的引用<ruleref>。这个规则包含了词组to London。因此,通过应用从根开始的规则,我们可以处理字符串show flights to London

规则引用在存在多个选择时很有用;在这种情况下,有多个可选的目的地。<one-of>标签允许指定可选项目。因此,例如,我们可以如下扩展目的地规则:

<rule id = "destination"> 
    <one-of>
          <item> to London </item>
          <item> to Paris </item> 
          <item> to New York </item> 
    </one-of> 
</rule>

实际上,由于单词 to 在此规则的所有短语中都是通用的,我们可以创建一个进一步的城市词汇规则引用,如下面更广泛的语法所示。通过这种方式,可以创建复杂的规则层次结构,以指定广泛可能的输入。

有时,单词或短语可能是可选的,或者可能重复多次。repeat 属性允许使用以下规格进行重复,其中 nm 是自然数:

<item repeat="n">        The item repeats n times
<item repeat="n-m">      The item repeats from n to m times
<item repeat="m-">       The item repeats at least m times

例如,对于一个五位数的邮政编码,项目应该如下所示:

<item repeat="5"><one-of><item>0</item><item>1</item> …<item>9</item></one-of></item>

<item repeat="0-1"> 指示某个项目是可选的。在以下语法中,flight_queryflightstime 规则指定了可选元素。这意味着有效的输入只需要 origindestinationdepart_day,其他信息是可选的。

给定各种标签和属性的组合,我们可以创建一个语法,允许如下输入:

  • 我想要周一早上从巴黎飞往纽约的航班

  • 让我看看周二从伦敦飞往巴黎的航班

  • 周一下午从纽约飞往伦敦

  • 周三从巴黎飞往伦敦

以下是可用于处理这些及其他广泛替代输入的语法:

<grammar root="flight_query">

<rule id="flight_query">
    <ruleref uri="verb"/> 
    <ruleref uri="flights"/> 
    <ruleref uri="origin"/> 
    <ruleref uri="destination"/>
    <ruleref uri="depart_day"/>
    <ruleref uri="depart_time"/>
</rule>

<rule id="verb">
      <item repeat="0-1">
        <one-of>
                <item><tag>show</tag>Show me</item>
                <item><tag>show</tag>I would like</item>
                <item><tag>show</tag>Are there any</item>
        </one-of>
    </item>
</rule>

<rule id="flights">
      <item repeat="0-1">
          <one-of>
                <item><tag>1</tag>a flight</item>
                <item><tag>N</tag>flights</item>
          </one-of>
    </item>
</rule>

<rule id="origin"> <item>from</item> <ruleref uri="city"/> </rule>
<rule id="destination"> <item>to</item> <ruleref uri="city"/> </rule>
<rule id="depart_day"> <item>on</item> <ruleref uri="day"/> </rule>
<rule id="depart_time"> <ruleref uri="time"/> </rule>

<rule id="city">
    <one-of>
          <item><tag>LHR</tag>London</item>
          <item><tag>CDG</tag>Paris</item>
          <item><tag>JFK</tag>New York</item>
    </one-of>
</rule>

<rule id="day">
    <one-of>
          <item><tag>M</tag>Monday</item>
          <item><tag>T</tag>Tuesday</item>
          <item><tag>W</tag>Wednesday</item>
    </one-of>
</rule>

<rule id="time">
    <item repeat="0-1">
          <one-of>
                <item><tag>a.m.</tag>morning</item>
                <item><tag>p.m</tag>afternoon</item>
          </one-of>
    </item>
</rule>

</grammar>

<tag> 用于返回与输入中识别的单词不同的项目的值。这对于处理同义词很有用,在这种情况下,具有相同意义的单词应该返回一个单一的值,而不是返回识别的文本字面值,或者返回在应用程序其他组件中更有用的值。以下是一个简单的例子:

<item><tag>M</tag>Monday</item>

在这里,如果处理单词 Monday,则返回值 M

更一般地,应用程序可能不需要对输入的完整转录就能进行进一步的处理,因为使用 <tag> 标签的某些中间(语义)表示可能就足够了,甚至可能更有效。通常,语义解释标签遵循 W3C 在 www.w3.org/TR/semantic-interpretation/ 提出的格式,它被解析和处理为 ECMAScript 对象。这意味着可以在语义标签中包含小段代码。

在本章中,我们将通过指定纯文本标签来进行非常简单的语义处理。例如,在上述语法中,我们包含了指定每个城市主要机场代码的标签,以便当句子包含 to Paris 时,destination 的语义解释是 CDG(查尔斯·戴高乐机场的代码),而 departure_time 的语义解释是 p.m 如果用户说 in the afternoon

统计自然语言理解

手工制作的语法耗时长,容易出错。开发具有良好覆盖范围和优化性能的语法需要相当的语言学和工程专业知识。此外,手工制作语法的规则不能轻松应对自然口语中不规则输入的特点。例如,对于识别出的单词I would like a um flight from Paris to New York on Monday no Tuesday afternoon,由于umno在规则中没有指定,我们的语法将失败。

统计语法是手工制作语法的替代品。统计语法是从数据中学习得到的,涉及收集和注释大量相关的语言数据。统计语法能够处理不规则输入,因为它们不需要与输入完全匹配,而是分配概率,表明结构或语义解释与输入的匹配程度。统计语法有不同类型。出于本章的目的,我们感兴趣的是一种语法,它可以根据文本字符串的输入或语音识别组件的结果返回语义解释。

统计语法的缺点之一是它们需要大量的训练数据。尽管已经为各种现实世界应用收集了语言数据语料库,但这些数据并不公开可用,而且对于个别开发者来说,购买它们的成本是难以承受的。即使数据可用,也需要付出相当大的努力来注释和训练语法。目前,针对统计语义语法的 API 很少,但一种可能性是由Maluuba公司提供的网络服务(www.maluuba.com),该公司为 Android 设备开发了一个同名的个人助手以及用于其统计语义语法的 API。

Maluuba API(称为nAPI)从输入中提取三种类型的信息,如下所示:

  • 类别:它表示句子的主题,例如天气、旅行、娱乐和导航。目前,它涵盖了 22 个类别。

  • 动作:它解释了在类别中要完成的特定动作或意图,例如,天气有诸如WEATHER_STATUS检查天气和WEATHER_DETAILS获取风速和湿度等详细天气信息的动作。

  • 实体:它提供了必须提取的关键信息,例如位置、日期范围和时间。

结果以结构化对象的形式返回。以下是示例:

用户输入:明天贝尔法斯特的天气如何

Maluuba{"entities":{"dateRange":[{"start":"2013-05-09","end":"2013-05-10"}],"location":["belfast"]},"action":"WEATHER_STATUS","category":"WEATHER"}

请注意,除了识别类别、动作和实体外,语法还解决相对引用,如 tomorrow(此示例于 2013 年 5 月 8 日提交)。此外,只要能从输入中识别实体,输入不必完全符合语法规则。因此,像 Weather Belfast tomorrow 这样的句子将返回相同的结果。

NLULib

我们实现了一个自然语言理解库,其中包含处理手工艺和统计语法的类和方法。我们创建了 NLU 类 (NLU.java) 来封装这两者,但它们也可以单独处理。

处理 XML 语法

HandCraftedGrammar.java 包含了用于解析 XML 语音语法的方法,检查一个短语在语法中是否有效,以及获取其语义表示。我们考虑了 XML 格式中标签的一个子集,具体如下:

标签 描述 可可能的子标签 可能的属性(带*的属性为必填项)
<grammar> 指定最高级别的容器 <rule> root*
<rule> 指定单词和短语的有效序列和结构 <one-of><item><ruleref> id*
<one-of> 指定一组替代项 <one-of><item><ruleref>
<item> 指定有效的序列,包含字面量和语义标签 <tag><one-of><item><ruleref> repeat
<tag> 以字面形式指定语义信息
<ruleref> 指定对另一条规则的引用 uri*

构造函数将 XML 解析为 Java 对象(请参阅代码包中 sandra.libs.nlu.nlulib.HandCraftedGrammarparse 方法),并将语法转换为正则 Java 表达式(请参阅代码包中 sandra.libs.nlu.nlulib.HandCraftedGrammarcomputeRegularExpression)。

parse 方法使用 XmlPullParser 将 XML 代码读取到对象中,类似于前一章开发的 VXML 解析器,但使用数组来跟踪嵌套项,因为在这种情况下,我们可以有相同的标签嵌套(例如,请参阅前面提到的 flights 示例,其中几个 <item> 标签包含在另一个 <item> 标签内)。

在此情况下,我们创建了类 AlternativeItemRuleRepeatRuleReference,分别用于保存解析 one-of不带重复参数的 itemrule带重复参数的 itemrule-ref 标签的结果。GrammarElement 类被定义为一个超类,以便当某个类拥有其他元素的集合时,可以使用 GrammarElement 类作为通配符。例如,Alternative 类包含一个表示不同可用选择的 GrammarElement 集合。

解析的结果是一组 Rule 对象的集合,作为 HandCraftedGrammar 类的属性保存。规则保存在一个 HashMap 中,其键是它们的 ID。例如,解析示例语法中的 flights 规则得到的结构在下图中表示。注意,为了简化生成便于语义解析的正则表达式,我们在识别过程中不是实时处理语法,而是在识别开始前将其解析为 Java 对象:

处理 XML 语法

在此阶段,HandCraftedGrammar 构造函数通过 parse 方法将 XML 语法解析为 Java 对象。然后,它使用这些对象通过 computeRegularExpression 方法计算代表语法的正则表达式。正则表达式是表示同样信息的另一种方式,但它们的优势在于我们可以直接使用 java.util.regex API 中的 PatternMatcher 类将用户输入的句子与语法进行匹配(该 API 的教程可以在 docs.oracle.com/javase/tutorial/essential/regex/ 找到)。

将 Java 对象作为 XML 与正则表达式之间的中间步骤,使得处理大量的嵌套元素和规则引用变得更加容易,并允许从最简单的项开始,以自底向上的过程生成正则表达式,直至最复杂的规则。

computeRegularExpression 方法(见代码包)通过为每个元素调用 getRegExpr() 方法获取与根规则及其所有引用规则相对应的正则表达式。这个方法将 GrammarElement 中包含的信息转换为正则表达式语法。例如,前一个图结构的转换结果是 ( (a flight) | (flights) ) {0, 1}

RuleReference 对象的情况比较特殊,因为它们是使用通配符 "xxREFurixx" 进行转换的,其中 uri 是被引用的规则名称。例如,depart_day 规则被转换为 xxREFtimexx

HandCraftedGrammar 类中的 solveReferences 方法用于解决所有引用。为此,从根开始逐步解析引用,获取整个语法的正则表达式。完整语法的解析结果如下:

(((Show me)|(I would like)|(Are there any)){0,1})(((a flight)|(flights)){0,1})(from ((London)|(Paris)|(New York)))(to ((London)|(Paris)|(New York)))(on ((Monday)|(Tuesday)|(Wednesday)))((((morning)|(afternoon)){0,1}))

此方法还跟踪语义标签。最初,它们被分配到对应的项,例如,语义表示 JFK 被分配到其对应的项目。然而,为了能够解释(即分配给 city 然后是 destination),它们必须分配给对应的规则。为此,我们使用了 SemanticParsing 类,它允许将标签与规则关联。

由于正则表达式将是用于检查传入短语有效性的唯一机制,并且会失去规则的结构,SemanticParsing类具有一个位置属性,该属性允许我们确定应该与触发标签的文本匹配的正则表达式的组。

Java 正则表达式使得GrammarElement的不同子类能够获取由括号分组元素的局部匹配结果,例如,在之前的正则表达式中,整个匹配的句子位于位置 0,(((Show me)|(I would like)|(Are there any)){0,1})位于位置 1,((Show me)|(I would like)|(Are there any))位于位置 2,(Show me)位于位置 3,(Are there any)位于位置 5,而(on ((Monday)|(Tuesday)|(Wednesday)))位于位置 20。

我们使用SemanticParsing对象来匹配每个这些组中的语义标签。这样,depart_time规则的SemanticParsing对象表示匹配组位于第 20 位,如果表达式是on Monday,则语义为M;同理,T代表on TuesdayW代表on Wednesday

因此,对于句子Show me flights from Paris to New York on Tuesday morning的语义表示如下:

Verb: Show
Flights: N
Origin: CDG
Destination: JFK
Depart_day: T
Depart_time: a.m.

obtainSemantics方法(参见代码包中的HandCraftedGrammar.java)用于检查输入是否与语法匹配,如果匹配,它的语义表示是什么。如果语法尚未初始化,即尚未计算语法的正则表达式(例如,在构造函数中语法的格式存在问题),此方法将引发异常。

为了验证传入的短语,我们使用 Java 匹配器自动检查该短语是否与正则表达式兼容。为此,我们忽略空白并使用小写,如下面的代码片段所示:

Pattern p = Pattern.compile(grammarRegExpression.replaceAll("\\s","").toLowerCase());
Matcher m = p.matcher(utterance.replaceAll("\\s","").toLowerCase());

然后,我们使用 Java 类MatchergroupCountgroup属性来获取与正则表达式中每个组匹配的短语,其中每个组是括号之间的模式。此信息随后与SemanticParsing对象中保存的语义信息和预期位置进行比较,并将语义表示保存在obtainSemantics方法的输出字符串中。

处理统计语法

要使用 Maluuba,您需要注册一个开发者账户。访问 Maluuba 的开发者网站(dev.maluuba.com/),点击立即注册并获得 API 访问权限的标签。注册与 Google、Facebook 或 GitHub 账户关联。填写表格后,您将被引导至一个可以创建应用程序的页面。点击创建应用程序,系统会要求您输入应用程序名称应用程序描述。出于当前目的,可以输入一些简单的内容,例如,将应用程序名称设为flights,并输入一些文本作为描述,然后您将获得一个 API 密钥。现在您可以使用这个密钥向 Maluuba 发送自然语言查询,可以通过浏览器或 Android 应用程序发送。按照以下步骤通过浏览器测试 API 的访问:

http://napi.maluuba.com/v0/interpret?phase=I would like a flight from belfast to london on Monday&apikey= <your apikey>

NLU.java向 Maluuba 发送一个字符串以获得语义解释。您可以更改KEY属性以使用您的开发者密钥。为了访问语法,它使用了前一章描述的XMLLib,因此可以通过异步任务安全地访问 URL。

结果作为字符串传递给调用方法。例如,对于短语I would like to go to London,它会返回以下值:

{"entities":"destination":["London"]},"action":"NAVIGATION_DIRECTIONS","category":"NAVIGATION"}

GrammarTest 应用程序

GrammarTest应用程序(sandra.examples.nlu.grammartest)展示了如何使用NLULib。它有一个简单的图形用户界面,用户可以选择要使用的语法类型(手工制作或统计),还可以选择检查文本检查语音识别按钮,以获得输入内容的语义表示。

检查文本的情况下,输入是通过键盘在TextView框中输入的。在检查语音识别的情况下,应用程序识别口语输入并为 10 个最佳列表生成结果。

在手工制作语法的情况下,将从指定位置读取 XML 语法。默认使用的语法是之前展示的那个。如果输入(输入文本或 N 个最佳结果中的每一个)在语法中,它会显示有效消息和语义表示;如果不是,它会显示无效消息(这些消息不是硬编码的,而是从Strings文件中获取的)。

在统计语法的情况下,使用 Maluuba 服务。在这种情况下,我们不限制输入,因此所有短语都被视为有效,并为它们中的每一个显示语义解释。

这个应用程序对于希望熟悉语法设计过程的开发者非常有用。下图展示了使用手工制作(左)和统计(右)语法的应用程序的两个屏幕截图:

GrammarTest 应用程序

概述

本章展示了如何创建并使用语法来检查用户的输入是否符合应用程序所需的词汇和短语。语法还用于从用户输入中提取对应用程序相关的概念的语义表示。介绍了两种类型的语法:一种是开发者手工设计的语法,以匹配应用程序的需求;另一种是从大量相关数据中学习得到的统计语法。手工设计的语法适用于可预测和定义良好的输入,而统计语法能提供更健壮的性能,并能处理可能不太规范的一广泛输入范围。

迄今为止的章节中,示例都假设使用的语言是英语,界面仅为语音。在第七章,多语言和多模态对话中,我们将探讨如何构建除了英语之外使用其他语言,并且在语音之外使用其他模态的应用程序。

第七章:多语言和多模态对话

到目前为止,所有示例中使用的语言都是英语,交互方式主要是语音。本章展示了如何将其他语言整合到应用程序中。该章节还探讨了如何构建利用多种模态的应用程序,例如,将语音与视觉显示结合使用。

多语言性

在第二章,文本到语音合成和第三章,语音识别中,我们已经提供了基础工作,使你能够轻松地开发多语言应用程序。使用TTSLib库(第二章,文本到语音合成)你可以指定用于语音合成的语言。现在我们只需要对ASRLib(第三章,语音识别)进行一些小改进,使其接受不同的语言进行语音识别(最初设置为设备的默认语言)。为此,我们在代码包中创建了ASRMultilingualLib(在sandra.libs.asr.asrmultilinguallib中)。

我们不能期望用户实现的语音识别中所有语言都可用。因此,在设置语言之前,有必要检查它是否是支持的语言之一,如果不是,则设置当前的首选语言。

为此,发送一个RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS有序广播,它返回一个 Bundle,从中可以提取有关首选语言(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE)和支持的语言列表(RecognizerIntent.EXTRA_SUPPORTED_LANGUAGES)的信息。这需要对ASR.java文件进行一些修改,在ASRMultilingualLib库中。

现在,在listen方法中,会检查以确保所选语言在语音识别器中可用,然后调用新的startASR方法。

startASR方法基本上包含了与库的前一个版本中的listen方法相同的代码,在那里创建了RecognizerIntent以开始监听。然而,它引入了一个新的参数,用于指定识别所使用的语言,如下面的代码行所示,该代码行从startASR方法中调用:

  intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);

提示

要检查设备上可用的语言,你可以点击谷歌语音搜索/语音搜索设置,然后点击语言查看完整列表。相应的代码是两个字母的小写 ISO 639-1 语言代码,如en.wikipedia.org/wiki/List_of_ISO_639-1_codes所示。

listen方法已经进行了更改,以检查语言的可用性。为此,它使用了广播的意图RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS。为了处理广播,ASRMultilingualLib引入了两个新类:LanguageDetailsChecker,它是BroadCastReceiver,以及OnLanguageDetailsListener,这是一个接口,用于指定处理广播结果的方法。这些类来自gast-lib项目(github.com/gast-lib),并在Greg MiletteAdam Stroud所著的《Professional Android™ Sensor Programming》一书中进行了描述,Wrox, 2012。在listen方法中,创建并发送了广播意图,并使用OnLanguageDetailsListener接口指定在处理广播后应执行的操作。在我们的例子中,会检查语言是否与任何可用的语言匹配,然后开始识别。

在对ASRLib进行了上述描述的更改之后,我们就可以开发用户指定语音识别和合成语言的应用程序了。作为一个示例,我们开发了SillyParrot应用,如下所示。它要求选择一种语言,并在识别用户需求后,用所选语言播放最佳匹配的内容。

多语言

尝试了SillyParrot应用后,你可能已经注意到在选择其他语言后,GUI 仍然显示英文,这有些奇怪。我们将在多模态部分进一步讨论应用程序视觉和口语部分的同步,但现在我们将先确保它们使用同一种语言。为了做到这一点,我们将利用res文件夹,其中包含xml文件,这些文件包含可以从活动引用的信息。

实现对不同语言响应的应用程序的过程通常被称为应用程序的本地化。本地化中的一个主要问题是提供不同语言的文本信息,但图片、布局和其他资源也可以进行本地化(请查看本网页上的教程,了解一个包含本地化图片的示例:www.icanlocalize.com/site/tutorials/android-application-localization-tutorial/)。为此,需要将文件包含在指定语言代码的特定目录中。

如果设备的地区设置为es-ES,并且代码引用了字符串R.string.mymessage,Android 将按照以下顺序在以下目录中查找它:

  1. res/values-es-rES/strings.xml

  2. res/values-es/strings.xml

  3. res/values/strings.xml

因此,如果你在墨西哥,而设备的默认地区设置为es-MX,那么它不会匹配来自西班牙的默认西班牙语(选项 1),但会匹配通用西班牙语(选项 2)。如果你在芝加哥,设备的地区设置为en-US,那么它会尝试在默认目录(选项 3)中找到字符串。

注意

不要忘记默认资源!

我们不能期望所有地区设置都能在用户的设备上使用(具体解释请见developer.android.com/reference/java/util/Locale.html),因此,不要忘记默认资源非常重要,因为当没有为所需地区设置提供 xml 文件时,Android 将从res/values/加载它们。

一个好的设计实践是将所有目标语言的的消息包含在默认文件夹中,这是我们预计大多数用户会使用的语言,然后尽量为其他语言创建尽可能少的资源文件。

例如,如果我们确定应用必须支持以下语言:美国和加拿大使用的英语,美国使用的西班牙语,以及加拿大使用的法语,以美国英语为主要语言。在这种情况下,美国英语的字符串应该放在默认文件夹(res/values/strings.xml)中。然后,只有在加拿大英语中不同的特定消息才会被包含在res/values/en-rCA/strings.xml文件夹中,因此无需为这两个地区复制所有字符串。西班牙语和法语也是类似处理,可以包含在res/values-ES/res/values-fr/文件夹中,如果需要考虑其他变体,则后来可以本地化为res/values-es-rUSres/values-fr-rCA/。Android 提供了一个清单,以规划如何本地化你的应用,你可以在这里找到:developer.android.com/distribute/googleplay/publish/localizing.html。注意在创建目录时,在国别代码前加上r,并使用-代替_

Parrot应用中,我们通过自动适应设备地区设置(Locale.getDefault().getDisplayLanguage();)改进了SillyParrot,这一做法符合前文所述的结构。请查看代码包中的项目目录结构。在 Android 开发者指南(developer.android.com/guide/topics/resources/localization.htm)中有示例说明如何在不同语言中测试本地化应用,不过你需要使用物理设备尝试不同的语言来测试语音识别部分。

多模态

如果一个应用使用了多种输入和/或输出方式,它可能被认为是多模态的。从这个意义上说,本书中介绍的所有应用都是多模态的,因为它们既使用了语音,也使用了图形用户界面进行输入或输出。

然而,在之前的例子中,模态并没有同步或用于提供处理相同信息片段的替代方式。例如,在第五章《表单填充对话》中的 MusicBrain 应用程序,输入是口头的,输出是视觉的,在本章中描述的 SimpleParrot 应用程序中,输入和输出都是口头的(需要语音识别和合成)和视觉的(选择语言和输入中的 按下说话 按钮以及输出的 通知 ),但它们对应于界面中的不同元素。

在本节中,我们将介绍如何开发多模态应用程序,在这些应用程序中,模态可以无缝结合以输入或输出数据,因此用户可以在特定时间选择最方便的模态。

为此,我们在第五章《表单填充对话》中展示的 FormFillLib 库基础上进行构建,通过考虑语音字段与 GUI 中不同元素的同步来增强它。这样,用户可以通过点击或使用语音来填充 GUI 中的所有不同元素。

主要思想是在语音表单中的字段与 GUI 中的不同元素之间建立对应关系。我们已经考虑了以下表格中显示的语音和 GUI 元素的组合,以便当一个元素对中的元素被填充时,另一个元素相应地被赋值。例如,如果用户被提示选择一个选项并且口头这样做,GUI 中的相应单选按钮或列表中的元素将被选中。同样,如果在单选按钮或列表中选择了元素,用户将不会被口头询问。

描述 语音字段 GUI 元素
开放输入,用户可以书写长文本或口头提供 可以直接使用谷歌语音识别器的结果 文本字段 ( EditText)
受限输入,用户可以在备选选项之间选择 应该使用手工制作的语法来限制选项 单选按钮组 ( RadioGroup) 或列表 ( ListViewSpinner)
受限的 yes/no 类型输入 应该使用手工制作的语法来限制选项为 yes/no 及其等价物(例如,true/false 复选框 (CheckBox)

从表中可以观察到,引入语法很重要,因为语音输入必须只接受 GUI 提供的选项。这就是为什么我们在第五章《表单填充对话》中展示的 VXML 解析器功能也增加了对语法的考虑。

为了实现这一目标,新的MultimodalFormFillLib库(参见代码包中的sandra.libs.dm.multimodalformfilllib)相较于FormFillLib库进行了几处修改,这些修改在代码包中的注释里也有说明。下一张图展示了如何在新库中解释口头对话的活动图。阴影部分是相较于第五章,表单填充对话中介绍的解释器的主要变化。

多模态

如前图所示,在MultimodalFormFillLib库中,并不是所有识别结果在所有情况下都被认为是有效的。如果我们查看之前引入的表格;如果语音字段对应于 GUI 中的单选按钮、复选框或列表,我们应该将字段的合法值限制为提供的选项。例如,如果我们的应用程序提供了一个包含巧克力、香草和草莓口味的冰淇淋选择列表,那么当询问用户他更喜欢哪种口味时,唯一有效的识别结果应该是这三种之一。这样,用户可以在屏幕上选择口味,或者口头说出口味,应用程序将自动更新另一种模态(填写语音字段或分别在 GUI 中选择选项)。

这个限制是通过语法实现的。现在,每个字段都包含有关其语法的详细信息(如果已指定的话,例如对于没有输入限制的文本字段,则无需指明语法)。当DialogInterpreter访问该字段时,如果存在语法,则只有语法中有效的识别短语才会被考虑;如果不是,则被视为不匹配。

注意事项

为了简化,我们只根据语法判断值是否有效,而不考虑其语义价值。然而,如果Field类中的isValid方法不仅仅计算布尔值,而是以某种方式处理语义(例如,遵循第六章,对话语法中提出的指导方针),这是可以实现的。

VXMLParser.java文件中,我们包含了<grammar>作为可能要解析的标签。此标签有一个必填的src属性,指明其位置。当解析器遇到该标签时,它会调用Field类中的setGrammar方法,将语法src作为参数传递。在Field.java中,我们包含了获取语法的 XML 内容的方法以及解析和查询它们的方法。setGrammar方法调用retrieveGrammar,后者访问urlsrc属性包含一个 URL)或从assets文件夹中读取语法(src属性仅包含语法的名称),并以字符串形式获取内容。然后processXMLContents方法使用第六章开发的NLULib库初始化手工制作的语法。对话语法。如果没有为特定字段指定语法,则任何识别结果都无需后续过滤即可接受。

我们在Field.java文件中增加了一个名为isValid的方法,该方法可以用来检查一个短语在语法中是否有效。当不使用语法时,无论值如何,isValid方法都返回true

所述行为在DialogInterpreter类的processAsrResults方法中编码,我们在这其中包含了检查识别器获得的 N-best 列表中的任何识别结果在语法中是否有效的代码。如果有效,则流程移动到下一个字段;如果无效,则保持在同一字段。

在前面的章节中使用语音界面时,用户无法删除他已经提供信息的字段。在使用多模态界面时,情况并非如此,因为用户可以在 GUI 中更改已提供的项目。我们更改了DialogInterpreter类中的moveToNextField方法,因此对话结束的条件不是访问了所有字段,而是所有字段都已填写(其值不为null)。检查表单中所有字段是否已填写的方法(allFieldsFilled)已包含在Form类中。同时,它将字段列表视为循环的,以便在遇到最后一个字段时,如果有未填写的字段,它将继续从第一个字段开始。

既然语法已准备好被考虑,我们可以看看多模态。我们创建了MultimodalDialogInterpreter类,它是DialogInterpreter的一个子类,包含一系列MultimodalElement对象。这些对象包含必须同步的口语和 GUI 字段对。

首先,我们确保口头部分的更改对 GUI 产生影响。每次口头填写一个字段时,都会调用oraltoGui方法(在DialogInterpreter中是抽象的,在MultimodalDialogInterpreter中实现),以在 GUI 中显示其值(请参见DialogInterpreter中的processResults方法)。此方法检查与口头字段对应的视觉元素的类型,并通过调用相应的方法相应地显示信息。在列表视图和下拉菜单中选择一个项目,在单选组中选中一个单选按钮,在文本字段中写入文本,或者选中或取消选中复选框,如前表所示,展示了语音字段和 GUI 元素的组合。

其次,我们确保视觉部分的更改对口头部分产生影响。因此,如果在 GUI 中填写了一个元素,应用就不应该口头询问。在MultimodalDialogInterpreter类中调用guiToOral方法,将来自 GUI 的值保存到口头字段中。

下面以MultimodalFormFillLib的用途为例,我们展示了SendMessage应用,如图所示,用户可以通过口头、使用图形用户界面(GUI)或两者结合的方式填写一些信息。这是一个模拟应用,因为它不会从用户的联系人列表中读取联系人,也不会发送任何消息,因为本章的重点在于接口。然而,根据这里提供的说明:developer.android.com/training/contacts-provider/retrieve-names.html,使用SMSManager发送短信,或者使用带有ACTION_SENDEXTRA_EMAIL的 Intent 发送电子邮件,只需更改SendMessage.java文件中的populateContactListsendMessage方法的实现即可完成这些功能。

多模态

SendMessage应用从assets文件夹中读取包含口头对话结构的VXML文件,如下代码所示:

  <form>

  <field name="contact">
  <prompt>Who is the recipient?</prompt>
  <grammar src="img/contact_grammar.xml"/>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said. Please say the name of one of your contacts </nomatch>
  </field>

  <field name="ack">
  <grammar src="img/ack_grammar.xml"/>
  <prompt>Do you need acknowledgment of receipt?</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said </nomatch>
  </field>

  <field name="urgency">
  <grammar src="img/urgency_grammar.xml"/>
  <prompt>Is the message urgent or normal?</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  <nomatch> Sorry, I did not understand what you said </nomatch>
  </field>

  <field name="message">
  <prompt>Please dictate the message</prompt>
  <noinput> Sorry, I could not hear you </noinput>
  </field>

  </form>

VXML文件和 GUI 中可以观察到,需要从用户那里获取四部分信息:收件人、是否需要回执确认、消息的紧急性(正常或紧急)以及要发送的消息。我们使用了所有支持的不同的 GUI 元素。如 VXML 代码所示,除了message字段外,所有字段都分配了语法。contact_grammar接受联系人名称,ack_grammar接受可以翻译为布尔值的短语,如yesnotruefalseurgency_grammar包含两个可能的值normalurgent,而message字段没有指定语法,以便接受最佳识别结果,允许用户自由交谈。

VXML 也可以像在第五章,表单填充对话中那样从网络上读取,但由于它与 GUI 紧密耦合,所以它不太可能发生很大变化,因此可以更方便地存储在assets文件夹中。例如,我们考虑到口头输入的有效值与 GUI 中显示的视觉选项相同。在上一个截图中,单选按钮显示文本正常紧急,因此,相应的口头字段只应接受值urgentnormal。开发者必须牢记,这对 VXML 和语法文件构成了相当大的限制。

SendMessage类是MultimodalDialogInterpreter的子类。在onCreate方法中,它调用了initializeGUIstartDialog方法。前者初始化所有的 GUI 元素,这些元素必须在它们的监听器中调用guiToOral方法(因为口头部分必须在 GUI 更改后进行更改)。后者解析口头形式,设置口头和视觉元素之间的对应关系(setMultimodalCorrespondence),并开始口头互动。口头对话控制互动的结束。当调用processDialogResults时,用户会被告知互动是否成功(在实际设置中,此方法会实际发送消息)。

请注意,口头部分将在读取并解析vxml文件后才会准备好。这就是为什么 GUI 必须等待这个过程完成后再尝试更新口头部分的原因。在设置 GUI 元素时(在setContactListsetAckCheckBoxsetUrgencyRadioGroupsetMessageEditText中)考虑到了这一点。

下一个截图显示了一个与SendMessage应用互动的示例,其中 GUI 的最终状态如前一个截图所示。在示例中,用户选择接收回执确认并发送紧急消息。这些信息由系统保存在口头字段中。从截图中可以看出,用户并没有被口头询问这些信息。然后,用户说出收件人的名字。由于这是有效的联系人之一,因此它被接受并在 GUI 的相应选择列表中显示。

最后,用户输入信息,结果在 GUI 中显示,而不会像以下截图所示那样先通过语法进行过滤:

多模态

概述

本章展示了如何将除英语以外的语言集成到应用中,使用与不同地域设置相关的资源。我们还了解了如何同步口头和视觉模态,以便用户在与应用互动时可以结合使用它们。

在下一章中,我们将介绍如何设计虚拟个人助手,使其能在您的设备上进行对话并执行一系列任务。

第八章:与虚拟个人助理的对话

在本章中,你将学习如何设计和开发三种类型的虚拟个人助理VPA);一种可以就一系列日常话题进行对话的交谈式伴侣,一种提供类型 2 糖尿病领域常见问题解答FAQs)的专业 VPA,以及一种增强了执行搜索查询、控制设备应用和调用网络服务能力的交谈式伴侣。

在本章结束时,你应该了解开发每种类型 VPA 所需的内容。

VPA 的技术要求

VPAs 通常能够执行以下任务:

  • 与网络服务互动以获取信息并执行交易;通过搜索查询,链接到如知识图谱之类的知识库,或参与交易性对话以及启动和管理设备上的应用,如联系人、日历、短信或时钟。

  • 就随机话题与用户进行对话,例如回答诸如你住在哪里?你多大了?你说什么语言?你喜欢什么类型的音乐?等问题。VPA 需要为每个问题提供某种形式的回应,以使对话能够持续进行,即使它没有对问题的正确答案。

第四章中展示的VoiceLaunch应用,简单语音交互演示了如何在设备上启动一个应用,而第五章中展示的MusicBrain应用,表单填充对话展示了如何实现使用表单填充的交易性对话。第七章中的多模态应用,多语言和多模态对话展示了单模态以及多模态对话。然而,这些应用仅执行单一任务,而 VPA 执行一系列不同的任务。

对于虚拟个人助手(VPA)的挑战是从用户的语音中确定用户请求的任务。观察下面图中的数据流,我们知道如何使用语音识别来确定用户说出的单词(第三章, 语音识别),以及给定一组要说的单词如何给出回应(第二章, 文本到语音合成)。现在,我们需要弄清楚用户所说单词背后的意图,并组织 TTS 要说的单词。可以使用来自口语理解的技术来确定用户的意图,如第六章,对话语法所示,尽管在本章中我们将探讨一种替代技术。当我们在对话管理组件中查看回应是如何生成的时候,将解释提供 TTS 要说的单词。

VPA 的技术

确定用户意图

想象以下用户输入:

  • 我想预订一张飞往伦敦的机票。

  • 明天的天气预报是什么?

  • 明天早上 7.30 叫我起床。

  • 你是什么类型的电脑?

假设我们希望将这些输入映射到前一个图中的对话管理框列出的四个功能上。我们该如何操作?

一种方法是使用统计分类系统将开放式的用户输入分类到一组固定的类别中,如第六章中的统计语法所示,对话语法。这是大规模商业系统采用的方法。例如,故障排除应用程序可能会接收如下输入:

  • 我无法访问我的电子邮件

  • 我上不了互联网

  • 有一个小图标显示没有互联网访问

尽管这些输入表达方式不同,但它们都可以被归类为与互联网连接相关的问题。创建这样的统计分类系统需要收集大量相关的语音数据,并确定一组固定的类别。识别出可以预测将语音分配给特定类别特征,如特定关键词或短语的出现,将使用机器学习技术来训练分类器。例如,短语如访问我的电子邮件上网可以作为指示互联网连接问题类别的特征。

这种方法的优点是健壮性和覆盖面。由于匹配是统计的,因此可以对可能不完全匹配类别的输入进行分类。因此,即使是非语法输入或可以用不同方式表达输入,仍然可以分配给一个类别。手工制作语法来分类这种开放式的输入将是一项艰巨的任务,因为在这种情况下,匹配必须是精确的。

统计文本分类的缺点是,需要大量的话语样本来训练系统,因为表达同一件事情的方式有很多种。在一个大系统中,可能需要收集和注释成千上万种典型的话语才能提供足够的覆盖面。这是一个昂贵的流程;尽管一旦系统经过训练,通常会产生准确的结果。

第二种方法,在输入更有约束和可预测的情况下是可行的,即编写语法进行分类,使用第六章中展示的技术,对话语法,并将解析的输出设置为相关类的一个赋值以及相关的值。例如,在设置闹钟的命令中,将输出赋给设备的闹钟功能并提取时间和日期。然而,手工制作的语法也面临着覆盖面的问题,并且需要不断添加额外的规则来处理无法用现有规则解析的输入。

第三种方法是使用预定义的模式匹配输入。这种方法已成功应用于许多商业聊天机器人和虚拟个人助理的实现中。缺点是起初需要创建大量模式以匹配所有可能的输入,同样存在潜在覆盖不足的问题。

做出适当的响应

应用程序对用户输入的响应根据已识别的用户意图而有所不同,大致可以分为以下几类:

  • 对于表单填充对话,需要启动对话,并可能需要几轮系统-用户交互来填充对话框架中必要的槽位,如第五章中所示,表单填充对话

  • 在涉及网络服务的情况下,响应将是对相关网络服务的调用,并提取一些文本由 TTS 朗读。

  • 对于涉及激活设备特性的任务,如拨打电话或设置闹钟,执行命令并提取一些文本由 TTS 朗读。

  • 最后,如果任务是进行对话,VPA 提供的对话响应是由本章介绍的应用程序中的 Pandorabots 聊天机器人系统生成的。

Pandorabots

Pandorabots 是一个基于开源的免费网络服务,使开发人员能够在网上创建和托管聊天机器人(请见www.pandorabots.com/)。目前,已有超过 221,000 个聊天机器人在此托管,涵盖多种语言。同时,还为商业开发提供了一项付费服务。许多移动设备上的 VPAs 都是使用 Pandorabots 创建的。这些包括 Pannous 的 Voice Actions(也称为 Jeannie)、Skyvi、Iris 以及 Pandorabots 自己的 CallMom 应用。CallMom 可以执行其他 VPAs 相同的任务,但还包括学习功能,因此它可以学习个人偏好和联系人,并且可以教会它纠正语音识别错误。

ALICE 2.0 是CallMom应用中可用的聊天机器人个性之一。其前身 ALICE,最初由理查德·S·华莱士博士于 1995 年开发,已在聊天机器人竞赛中赢得了众多奖项,包括授予被认为是最像人类的年度竞赛聊天机器人的洛布纳奖。2013 年洛布纳竞赛的四名决赛选手中有三个使用了 Pandorabots 技术。原始 ALICE 中的大部分知识都是硬编码的,现在已经过时。例如,对英格兰的总理是谁?的回答仍未从原始的托尼·布莱尔更新。ALICE 2.0 通过从外部服务获取事实信息,克服了这些不足,其他 VPAs(如 Siri)也是如此。关于 Pandorabots 及聊天机器人的更多信息可以在 Pandorabots 网站和由华莱士创立的 ALICE A.I.基金会网站(www.alicebot.org)上找到,该基金会旨在推广人工智能标记语言AIML)及聊天机器人技术的发展。

AIML

AIML 是一种基于 XML 的语言,用于指定聊天机器人的对话行为。AIML 中的基本单位是category,它由patterntemplate属性组成。以下是示例:

<category>
       <pattern> WHAT ARE YOU </pattern>
       <template>
             I am the latest result in artificial intelligence, which       	             can reproduce the capabilities of the human brain with 	   	             greater speed and accuracy.
       </template>
</category>

在此示例中,用户的输入与模式WHAT ARE YOU匹配,并生成模板中提供的回应。通过使用成千上万的类别和通配符以实现更灵活的模式匹配,可以产生相当复杂对话。

通配符用于匹配输入中的一个或多个单词的字符串。以下是一个简单的通配符*示例:

<category>
       <pattern>ABBA *</pattern>
       <template>They were a great band in the 70's.</template>
</category>

在此示例中,任何以ABBA开头的输入都将匹配此类别,结果输出为They were a great band in the 70's(他们 70 年代是一个很棒的乐队)。

通常会有多种不同的方式表达同一件事情,而不是将每个都列为模式或模板对,可以使用<srai>标签(意味着符号简化),如下例所示:

<category>
       <pattern> HOW DID YOU GET YOUR NAME </pattern>
       <template> <srai>WHAT DOES ALICE STAND FOR </srai> </template>
</category>

在这里,输入HOW DID YOU GET YOUR NAME被视为与WHAT DOES ALICE STAND FOR具有相同含义,并且<srai>标签表示它们应具有相同的答案。

如果没有与输入匹配的模式;换句话说,机器人不理解对它说的话;那么将应用最终默认类别。对于这个类别,模式是 *,默认响应是 I have no answer for that。机器人编程的艺术在于提供一系列的响应,以帮助保持对话的进行。例如,告诉我更多关于你自己的事情 应该使用户说出可能有匹配模式的话。最终默认类别还可以用来捕获任何模式都无法预测的输入,并处理它以确定是否可以以其他方式处理。例如,它可以被发送到搜索引擎或在线知识源以获取响应。

模式匹配是通过使用一种匹配算法来完成的,该算法通过类别进行图搜索。首先整理 AIML 模式中的文本,去除不必要的标点符号,转换为大写,并扩展缩略词,例如将 I'll 扩展为 I will。

就模板而言,除了指定响应的文本外,还可以执行简单的计算。例如,可以检索属性值,这些属性值是指向机器人属性的常数,如 agenamelocation。还有处理谓词的方法,这些谓词是在对话过程中设置的变量,例如对话的主题,或者如何处理代词以指代特定的词。这些计算有助于使对话更加自然。

本书不涵盖对 AIML 的全面介绍。完整的文档可以在 Pandorabots 网站上找到(www.pandorabots.com)。

使用 oob 标签添加附加功能

<oob> 标签是最近添加到 AIML 中的,支持在移动设备上使用。OOB 是 out of band 的缩写,这是工程中的一个术语,指的是在单独的隐藏通道中进行对话。在 AIML 中,<oob> 标签可以用来向设备发送命令,例如拨打电话、发送短信、启动应用程序等。标签的内容不是用户收到的响应的一部分,因此可以在这里包含诸如 URL、应用程序名称和其他关键字等内容,以便在 Android 代码中提取并根据需要进行处理。以下是一些使用 <oob> 标签的模式示例:

<category>
       <pattern> * </pattern>
       <template> 
             <oob><url><search><star/></search></url></oob>
             Please wait while I try to find an answer from Google
       </template>
</category>

在此示例中,如果机器人没有在其 AIML 文件中找到用户输入的匹配项,则响应被标记为 <search>。搜索查询的内容是 <star/>,其值是输入中说出的话。此阶段以及以下示例的代码可以在代码包中的 VPALib (sandra.libs.vpa.vpalib) 中找到。位于 <oob> 标签外的文本可以用于其他目的;在这些应用程序中,它被发送到 TTS 并用于语音输出。

下面的示例展示了输入WIKIPEDIA如何设置 Wikipedia 的 URL 启动:

<category>
       <pattern>WIKIPEDIA</pattern>
       <template>
             <oob><url>http://www.wikipedia.org</url></oob> 
             Opening Wikipedia
       </template>
</category>

在以下示例中,匹配的输入GMAIL设置了 Google 邮箱的启动:

<category>
       <pattern>GMAIL</pattern>
       <template>
             <oob><launch>com.google.android.gm</launch></oob>
             Launching Gmail.
       </template>
</category>

VPALib 库

我们创建了一个库,其中包含与 Pandorabots 连接的代码。任何想要集成代理的应用只需处理界面方面的问题,比如 GUI 外观以及语音识别和合成的控制。

VPALib库使用ASRLibTTSLibXMLLib来管理语音识别、语音合成和检索网络内容时的异步任务(你可以在VPALib项目中的libs文件夹中查看这些库)。这样,它可以专注于管理 VPA 行为的代码。

VPALib 库中的主要类是Bot类,它向 Pandorabots 网站发送查询,解析结果,并在设备上执行相应的操作。

创建新的机器人时,我们必须向构造函数指定一个 ID。这是 Pandorabots 网站上的 ID(例如,Jack 的 ID 为d7b695cf0e344c0a)。同时,我们可以指定它的专业主题。例如,如果机器人专门提供有关 NFL 的信息,则使用字符串NFL进行网络搜索。因此,如果用户询问比赛,机器人将搜索 NFL 比赛,而不是任何类型的比赛。

initiateQuery方法将把对应于用户输入的文本发送到 Pandorabots 网站上的机器人。这个过程涉及到在查询中插入%20作为空格,因为需要以表单-URL 编码的形式发布值,创建查询并将其作为后台异步任务发送到 Pandorabots,使用第五章,表单填充对话中描述的XMLLib文件夹。

AsyncTask的结果在processXMLContents()方法中处理。来自 Pandorabots 的响应输出使用XMLPullParser进行解析,其方式与我们解析VXML(第五章,表单填充对话)或XML语法文件(第六章,对话语法)类似。

响应内容可能包含或不包含<oob>标签。如果不包含,机器人唯一要做的就是解析和合成响应。为此,它必须提取<that>标签内的信息。以下是AsyncTask结果的示例:

<result status="0" botid=" d7b695cf0e344c0a"  
custid="c6015de7be06c599">
       <input> what languages do you speak </input>
       <that> C, Java, Lisp, SETL and English </that>
</result>

在此情况下,该方法将调用 TTS 引擎来合成C, Java, Lisp, SETL 和 English

注意

Google 的 TTS 语音的性别无法通过编程选择。我们使用了英国英语语音,目前是男性,但可能会出现 VPA 选择的性别与用户设备中的声音不匹配的情况。目前,这只能通过使用其他 TTS 引擎来解决。

当存在 <oob> 标签时,我们假设它们在 AIML 文件中已经标记为以下标签之一:<search><launch><url><dial>。当遇到 <search> 时,将调用 googleQuery 方法,在 Google 搜索引擎中启动查询。当遇到 <launch> 时,将调用 launchApp 方法,在设备中启动一个应用。类似地,当遇到 <url> 标签时,将调用 launchUrl 方法以打开指定的网页。最后,当处理 <dial> 时,它会调用 placePhoneCall 方法,拨打电话。

launchApplaunchUrl 方法与第四章简单语音交互中介绍的方法相似,不同之处在于,那里的代码更为复杂,因为它使用了相似性函数。本章展示的方法更为简单,以便专注于将 <oob> 标签作为一种技术,用于识别用户输入中的这些命令。

更一般地说,开发者需要决定是否在 AIML 中标记不同的功能,然后处理它们,或者采用不同的方法来识别输出是文本还是需要执行的命令。

创建一个 Pandorabot

下节将要描述的应用使用的是 Pandorabots 网站提供的 AIML 代码。因此,我们必须采取的第一步是在 Pandorabots 网站注册一个账户。

注册后,进入网站并点击创建一个 Pandorabot选项。这将打开一个页面,要求你为你的机器人命名(我们称我们的为Jack)。从多个选项中选择你的启动 AIML,包括无初始内容,Pandorabot 从零知识开始的选项。为了这个示例的目的,选择一个 AIML 集合(例如,Dr Wallace 的 A.L.I.C.E – 2002 年 3 月)。这将带你去到你的 Pandorabot 的页面,在那里你可以探索许多选项。

要查看 AIML 文件,请点击AIML,然后点击列出的任何文件。这将让你了解可以用来提供对话响应的众多类别。

要尝试使用预存在的 AIML 的机器人,请点击训练,然后提问以查看机器人的回答。这将打开一个训练页面,指示找到与你的输入匹配的 AIML 文件,以及帮助完善机器人的其他各种选项。预存在的 AIML 文件无法修改,但任何新的类别都将存储在名为 update.aiml 的文件中。

若要让你的机器人供其他人使用,你必须发布它(或者如果你对 AIML 代码做了任何更改,重新发布)。回到你的机器人主页,点击发布。这将加载一个页面,显示你的机器人的位置。

点击此链接将打开一个公开页面,任何拥有链接的人都可以访问并与您的机器人互动。这样,您可以收集与机器人互动的进一步数据,并在日志文件中查看,从而进一步优化机器人的响应。

在此示例中,我们将提供一个在 Android 设备上运行的接口,并使用语音与机器人互动。注意 botid 将很重要,因为从您的 Android 程序连接到您的机器人时需要此 ID。

示例 VPAs – Jack、Derek 和 Stacy

为了说明 VPALib 的使用,我们开发了三个机器人:Jack、Derek 和 Stacy。从它们各自的软件包(见代码包)中可以看出,它们具有相同的结构;一个实现 ASR 和 TTS 的主活动,并创建一个机器人实例,以及一个简单的图形界面。这显示了拥有一个处理与 Pandorabots 连接的库的便利性。主要区别在于它们对应的 AIML 文件。

Pandorabots 中机器人的链接如下:

Jack 和 Derek 的界面在以下屏幕截图中显示:

示例 VPAs – Jack、Derek 和 Stacy

Jack 是一个通用型 VPA,而 Derek 是一个专业型 VPA。专业型 VPA 执行诸如提供客户服务或回答客户查询等任务。VPA 的知识库以 AIML 编码形式存储,作为一组问题-回答对,类似于常见问题解答。我们在 AIML 中开发了一套关于 2 型糖尿病的基本问题。Derek 可以回答有关症状、原因、治疗、对孩子的影响和并发症等话题的问题。

Stacy 具有与 Jack 相同的 AIML 文件,但除此之外,还有一个包含带有 <oob> 标签的类别的文件,以执行如下小部分功能:

  • 向 DBPedia 发送搜索词,获取响应并朗读。如果无法从 DBPedia 获取文本,则将搜索词发送到 Google 搜索。

  • 调用网页(维基百科或 Facebook)。

  • 在设备上启动应用(时钟、日历、电话)。

执行这些功能的代码可以在代码包中的 VPALib 中找到。

请注意 Jack.java (Jack 项目) 和 Derek.java (Derek 项目) 文件中 Bot 类实例化的不同方式。在 Jack 的情况下,没有 specializedTopic,而在 Derek 的情况下,是 Type 2 diabetes,使用了 Bot 类 (VPALib 项目) 的两种替代构造函数。

运行几次 Derek 应用后,很快就会发现对您口语输入的误识别会导致机器人用默认响应“我对那个没有答案”来回应。这可能会在几次后变得令人烦恼,而且还有一个潜在有效查询丢失的问题,因为识别出的词汇与 AIML 代码中的任何模式都不匹配。例如,在与 Derek 的一次互动中,我们说“对我的孩子有什么风险”,应用返回的识别结果是“其他风险孩子”,这并没有匹配我们的任何模式。如果提取这个短语中的有用部分,“风险”和“孩子”,并将它们与Type 2 diabetes这个术语结合,发送一个扩展的搜索查询,可能会更有可能找到相关结果。这已经通过在 AIML 代码中使用一个包含 <oob> 标签的类别来实现,该标签与 VPALib 中的一个函数相链接。

替代方法

这些示例展示了使用 Pandorabots 对话引擎和 <oob> 标签作为资源可以执行的一些功能。还可以添加其他功能(有关带有示例的 <oob> 标签的完整列表,请参见code.google.com/p/aiml-en-us-pandorabots-callmom/wiki/CallMomOOBTags)。

以下是一些此方法的局限性:

  • 对于一般的对话交互,应当有足够的默认响应来处理输入没有匹配的情况。这不是一个严重的问题,因为现有的 AIML 代码中有许多可以使用或适配的示例。

  • 对于专业的虚拟个人助手(VPA),即使如前一个示例中那样将查询扩展,例如添加Type 2 diabetes这样的词汇,可能也不足以使用相对临时性的方法将未匹配的输入发送到搜索引擎。这种方法很大程度上依赖于搜索引擎返回与用户查询相匹配的 URL 列表,并且搜索文本准确地反映用户的意图。

  • <oob> 标签的使用取决于创建正确匹配用户输入的模式。例如,用户应该说出类似于“日历”、“Gmail”或其他关键词来指示预期的设备功能。这些 AIML 代码中的模式必须与使用它们的 Java 代码紧密相连。实现这一点的库将是一个有用的补充。

  • 这种方法不便于网站的一般化处理。例如,每个网站(如维基百科)都必须在 AIML 的一个类别中作为一个模式单独编码。

一种更复杂且可扩展的方法来确定用户的意图,可以是使用第六章中展示的对话语法类型的语法。可以设计一个手工打造的语法来识别输入中的关键词或短语,或者开发一个适当的统计语法来更健壮地处理输入。未通过这种方式提取的输入,则可以作为对话输入,传递给如 AIML 这样的资源。

Pandorabots 团队正在解决其中一些问题,但目前的解决方案尚未作为开源代码提供。新的发展将在本书的网站上发布。

概述

在本章中,我们了解了如何使用 Pandorabots 聊天机器人技术提供的资源来开发不同类型的 VPAs。基于这里描述的应用程序,你现在应该能够为你的目的设计和开发 VPAs。第九章中提出了一些建议,关于如何在这些示例基础上构建,并包含本书中描述的更多技术。

第九章:更进一步

在本书的学习过程中,你已经学会了如何使用谷歌语音 API,并应用了一系列技术,如第八章中描述的应用程序。然而,为了保持这些应用程序的相对简单,一些更高级的技术并没有被使用。本章提出了将这些技术包含在更高级虚拟个人助理中的方法。

开发更高级的虚拟个人助理

系统输出,如第二章中介绍的 TTS,可以通过包含不同的声音、语言和模式来增强文本到语音合成,例如,呈现不适合语音输出的信息。

第三章中引入的用户输入语音识别技术可以通过多种方式进行开发:

  • 通过包含第四章中引入的相似度测量,将识别结果与可能实际是用户所说的相似词汇进行比较

  • 通过利用其他技术来增强识别结果,如书中Greg MiletteAdam Stroud所著的《Professional Android Sensor Programming》第十七章所述的词干提取

  • 通过结合置信度测量来支持决定是否使用确认,如第四章所述

  • 通过利用 n-best 识别结果来确定其中一个结果是否比最佳识别结果更可信

  • 通过允许使用其他语言输入,如第七章所述

  • 当语音识别变得不可靠或出现其他问题时,切换到其他模态(见第七章)

通过在第八章《与虚拟个人助理的对话》中展示的应用程序对用户输入的解释,其中使用了用户输入与 AIML 类别中的模式进行简单匹配,并从该类别的模板中检索应用程序的响应。使用 Oob 标签捕获诸如搜索或 URL 之类的关键词,以激活诸如搜索或启动网页等操作。更高级的分析用户输入的方法将涉及使用第六章《对话语法》中描述的技术进行过滤,通过开发语法来解析语音识别引擎返回的响应并将其分类为不同的类型。

通过对话管理选择应用程序的下一个动作及其口头回应可以在以下几种方式中进行改进:

  • 在第八章《与虚拟个人助理的对话》中展示的应用程序没有实现表单填写。包括如第五章《表单填写对话》和第七章《多语言和多模态对话》中描述的表单填写对话,会将诸如I want to book a hotel in Barcelona的用户输入识别为交易请求,并启动适当的表单填写对话来处理。

  • 可以添加其他知识源,例如使用 Freebase 等知识源来支持问题回答。Freebase 是谷歌知识图谱的开放核心。有关如何获取 API 密钥的文档和说明可以在网站developers.google.com/freebase/上找到。

目前,用户界面是语音输入输出与传统图形用户界面的结合。许多商业网站使用谈话头像技术(或虚拟形象)为其 VPA 提供一个个性角色。一个值得增强的功能是构建一个包含谈话头像并包含个人信息的 VPA,以赋予 VPA 一个个性角色。有几个选项可用,其中一些在 Pandorabots 网站上有所提及(www.pandorabots.com/)。

摘要

本章节提出了各种建议,以扩展本书中展示的示例。我们鼓励您测试、修改并在书中提供的代码中进行尝试。在本书的网站 lsi.ugr.es/zoraida/androidspeechbook 上,您将找到代码的源文件,以及新项目的进一步想法和各种有趣资源和更新。语音技术是一个激动人心的主题,为 Android 开发者提供了无限可能。我们邀请您深吸一口气,沉浸其中!

附录 A. 结语

阅读完这本书后,你将知道如何实现可以说话和听的应用程序。从开发你可以向朋友和家人展示的小型个人应用程序开始。你也可以向潜在的雇主或客户展示这些应用程序。以下是我学生实现的一些小型个人应用程序。构建一个这样的应用程序,以展示你在 Android 移动设备上使用语音技术所能做到的事情。

互动贺卡

不仅通过文本,还可以通过语音传递你的信息(录制的声音比合成的声音更具个性化)。收集接收贺卡的当事人的回应,并将它们通过电子邮件发送给你自己。

互动食谱

口头呈现食谱的食材和烹饪说明,以及视觉上的辅助准备。厨师通过语音导航说明。一位学生用她祖母录制的语音说明替换了合成的说明。因此,烘焙祖母的苹果派的说明是用祖母的实际声音呈现的。真甜!

选择你自己的冒险故事

用你自己的声音录制童话故事片段。在片段之间加入语音菜单,询问听众选择下一个片段。你的孩子可以听到你给他们讲睡前故事,并指导故事角色的行动。

语音闪卡

向听众提出简短的问题,听众通过语音回答。非常适合学习乘法表、重要人物的名字、历史日期以及外语单词。

电话应答系统

询问来电者他们来电的目的以及他们想与谁交谈。使用此应用程序过滤你的电话来电并为你的家庭成员记录特定信息。

旅行指南

使用 GPS API 确定你的移动设备的位置,并添加用户可以看到的照片、图形和地标描述。使用 GPS API 定位手机并读取/显示有关其当前位置的信息。或者虚拟探索你不能去的地方。一位学生使用来自 NASA 的照片开发了一个宇宙旅行指南。

音频解说

为你的相册、最近的旅行、你参加的婚礼,甚至你儿子或女儿的比赛添加解说。

展示你的创造力 利用语音技术增强现有应用程序或创建新的应用程序。如果你是学生,可以将你的语音应用程序提交给应用语音输入/输出协会AVIOS)的学生竞赛 www.avios.org/。将你的语音应用程序提交到 Google 商店,play.google.com/store。向世界展示你能做什么!

詹姆斯·A·拉尔森

Larson 技术服务公司的副总裁兼创始人

posted @ 2024-05-23 11:09  绝不原创的飞龙  阅读(23)  评论(0编辑  收藏  举报