Windows Phone

通过语音命令让 Windows Phone 8 应用程序具备语音控制功能

F Avery Avery

下载代码示例

最近的一天晚上,我要在下班后与一位老朋友碰面,但我迟到了。 我知道他已在开车驶向集合地点,因此,给他打电话会有问题。 尽管如此,在我冲出办公室向我的车跑去时,我抄起了我的 Windows Phone 并按住“开始”按钮。 在我听到“耳标”语音提示时,我说:“Text Robert Brown”(给 Robert Brown 发短信),在短信应用程序启动后,我说:“Running late, leaving office now”(我迟到了,正离开办公室),之后我按下“发送”来发送该短信。

如果在内置的短信应用程序中没有语音功能,我就只能停止奔跑,在沮丧中笨手笨脚地发送短信,因为我发现我的胖手指很难使用小键盘,并且在跑步时很难阅读屏幕上显示的内容。 使用语音到文本功能节省了我的时间,消除了我的挫败感,不再使我感到不安。

Windows Phone 8 为开发人员提供了与之相同的语音功能,以便通过语音识别和文本到语音与其用户交互。 这些功能支持在我的示例中阐释的两种情形: 从手机上的任何位置,用户都可以说出一个命令来启动应用程序,并且只用一个语句就可以执行操作;在打开应用程序后,手机就可以通过捕获说话者所说语句中的命令或文本并且以声音方式向用户呈现文本以便提供通知和反馈,与用户进行对话。

称作语音命令的功能支持第一种情形。 为了实现此功能,该应用程序提供一个语音命令定义 (VCD) 文件,以便指定该应用程序能够处理的命令集。 在通过语音命令启动该应用程序后,它将接收查询字符串中的参数,例如可用于执行用户指定的命令的命令名称、参数名称和识别的文本。 本篇文章由两部分构成,第一期解释如何在 Windows Phone 8 上在您的应用程序中启用语音命令。

在第二期中,我将介绍应用程序中的语音对话。 为支持语音对话,Windows Phone 8 提供了一个 API,以便进行语音识别和合成。 该 API 包含一个用于确认和消除的默认 UI 以及用于语音语法、超时和其他属性的默认值,从而只需几行代码就可以将语音识别功能添加到某个应用程序中。 同样,语音合成 API(也称作文本到语音或 TTS)可以轻松地针对简单情形进行编码;它还提供高级功能,例如通过万维网联合会语音合成标记语言 (SSML) 进行微调操作,以及在手机上已有或者从市场下载的最终用户语音之间进行切换。 请继续关注后续文章中对此功能的详细探讨。

为了演示这些功能,我开发了一个名为 Magic Memo 的简单应用程序。 您可以通过按住“开始”按钮并在听到提示时说出一个命令,启动 Magic Memo 并执行该命令。 在该应用程序中,您可以使用简单的听写方法来输入您的备忘录,或者使用语音在应用程序内导航和执行命令。 在整篇文章中,我将说明实现这些功能的源代码。

在应用程序中使用语音功能的要求

假定您的开发环境满足用于开发 Windows Phone 8 应用程序和在手机仿真器上进行测试的硬件和软件要求,该 Magic Memo 应用程序应在开机时即可使用。 截止到本文发稿时,应满足的要求如下:

  • 64 位版本的 Windows 8 Pro 或更高版本
  • 4GB 或更大的 RAM
  • BIOS 支持的第二级地址转换
  • Hyper-V 已安装并且正在运行
  • Visual Studio 2012 Express for Windows Phone 或更高版本

与以往一样,在尝试开发和运行您的应用程序之前,最好查看 MSDN 文档以便了解最新要求。

在您从头开发自己的应用程序时还需要记住三件事:

  1. 确保设备麦克风和扬声器正常工作。
  2. 通过在属性编辑器中选中相应框或通过手动在 XML 文件中包含以下内容,将用于语音识别和麦克风的功能添加到 WpAppManifest.xml 文件中:
       <Capability Name="ID_CAP_SPEECH_RECOGNITION"/>
       <Capability Name="ID_CAP_MICROPHONE"/>
  1. 在尝试语音识别时,您应该捕获在用户尚未接受语音隐私策略时引发的异常。 随附示例代码下载中的 MainPage.xaml.cs 中的 GetNewMemoByVoice Helper 函数提供了一个示例,阐释如何执行此操作。

应用场景

对于任何智能电话,一个常见的使用情形是启动应用程序并执行单个命令,之后可以选择继续执行更多命令。 手动执行此操作需要完成若干步骤: 查找应用程序,导航到正确位置,查找按钮或菜单项,点击该按钮或菜单项,等等。 许多用户都觉得这些步骤令人生厌,甚至在他们已熟悉这些步骤后也是如此。

例如,为了在 Magic Memo 示例应用程序中显示已保存的备忘录(例如,第 12 条 备忘录),用户必须找到并启动该应用程序,点击“View saved memos”(查看已保存的备忘录),然后向下滚动直至显示所需备忘录。 下面是相比之下使用 Windows Phone 8 语音命令功能的体验: 该用户按住“开始”按钮并且说出“Magic Memo, show memo 12”(Magic Memo,显示第 12 条备忘录),之后该 Magic Memo 应用程序将启动并且在消息框中显示所需备忘录。 甚至对于这个简单的命令,也可以明显减少用户交互。

用户需执行三个步骤以便在应用程序中实现语音命令,而用于处理动态内容的第四步是可选的。 以下各节介绍了这些步骤。

指定要识别的语音命令

用于实现语音命令的第一步是在 VCD 文件中指定要听到的命令。 VCD 文件是用简单的 XML 格式创作的,由 CommandSet 元素集合构成,其中每个元素都具有包含要听到的短语的 Command 子元素。 图 1 中所示是来自 Magic Memo 应用程序的一个示例。

图 1 用于 Magic Memo 应用程序的语音命令定义文件

<?xml version="1.0" encoding="utf-8"?>
<VoiceCommands xmlns="https://schemas.microsoft.com/voicecommands/1.0">
  <CommandSet xml:lang="en-us" Name="MagicMemoEnu">
    <!-- Command set for all US English commands-->
    <CommandPrefix>Magic Memo</CommandPrefix>
    <Example>enter a new memo</Example>

    <Command Name="newMemo">
      <Example>enter a new memo</Example>
      <ListenFor>Enter [a] [new] memo</ListenFor>
      <ListenFor>Make [a] [new] memo</ListenFor>
      <ListenFor>Start [a] [new] memo</ListenFor>
      <Feedback>Entering a new memo</Feedback>
      <Navigate />    <!-- Navigation defaults to Main page -->
    </Command>

    <Command Name="showOne">
      <Example>show memo number two</Example>
      <ListenFor>show [me] memo [number] {num} </ListenFor>
      <ListenFor>display memo [number] {num}</ListenFor>
      <Feedback>Showing memo number {num}</Feedback>
      <Navigate Target="/ViewMemos.xaml"/>
    </Command>

    <PhraseList Label="num">
      <Item> 1 </Item>
      <Item> 2 </Item>
      <Item> 3 </Item>
    </PhraseList>
  </CommandSet>

  <CommandSet xml:lang="ja-JP" Name="MagicMemoJa">
    <!-- Command set for all Japanese commands -->
    <CommandPrefix>マジック・メモ</CommandPrefix>
    <Example>新規メモ</Example>

    <Command Name="newMemo">
      <Example>新規メモ</Example>
      <ListenFor>新規メモ[を]</ListenFor>
      <ListenFor>新しいメモ</ListenFor>
      <Feedback>メモを言ってください</Feedback>
      <Navigate/>
    </Command>

    <Command Name="showOne">
      <Example>メモ1を表示</Example>
      <ListenFor>メモ{num}を表示[してください] </ListenFor>
      <Feedback>メモ{num}を表示します。
</Feedback>
      <Navigate Target="/ViewMemos.xaml"/>
    </Command>

    <PhraseList Label="num">
      <Item> 1 </Item>
      <Item> 2 </Item>
      <Item> 3 </Item>
    </PhraseList>
</CommandSet>
</VoiceCommands>

下面是用于设计 VCD 文件的指导原则:

  1. 使命令前缀在发音上不同于 Windows Phone 键盘。 这将有助于避免您的应用程序与内置电话功能混淆。 对于美式 英语,关键字为 call、dial、start、open、find、search、text、note 和 help。
  2. 使您的命令前缀成为您的应用程序名称的子集或自然发音,而不是完全不同。 这将避免用户混淆并且减少错误地将您的应用程序识别为某个其他应用程序或功能的可能性。
  3. 请记住,识别要求对命令前缀的精确匹配。 因此,最好使命令前缀简单好记。
  4. 为每个命令集提供一个 Name 属性,以便可以在您的代码中访问该命令集。
  5. 将 ListenFor 元素保持在发音上彼此不一样的不同 Command 元素中,以便降低错误识别的可能性。
  6. 确保相同命令中的 ListenFor 元素采用不同方式指定相同命令。 如果一个命令中的 ListenFor 元素与多个操作相对应,则将这些元素拆分成若干单独的命令。 这可以更方便地处理您的应用程序中的命令。
  7. 请记住以下限制: 一个命令集中 100 个 Command 元素;一个命令中 10 个 ListenFor 条目;总共 50 个 PhraseList 元素;并且在所有 PhraseList 上总共 2,000 个 PhraseList 项。
  8. 请记住,对 PhraseList 元素的识别要求精确匹配,而不是部分匹配。 因此,若要识别“Star Wars”和“Star Wars Episode One”,您应该将这两项都作为 PhraseList 元素包含。

在我的示例中有两个 CommandSet 元素,每个元素都具有不同的 xml:lang 和 Name 属性。 每个 xml:lang 值只能有一个 CommandSet。 Name 属性必须也是唯一的,但是仅受到为 Name 属性指定的值的限制。 尽管是可选的,但强烈建议您包含 Name 属性,因为您将需要该属性以便从您的应用程序代码访问 CommandSet 来实现步骤 4。 还要注意,一次对于您的应用程序只能有一个 CommandSet 处于活动状态,也就是说其 xml:lang 属性精确匹配当前全局语音识别器的属性(由用户在 SETTINGS/speech 中设置)的 CommandSet。 您应该为您预期您的用户在其市场中要求的任何语言都包含 CommandSet。

下一件要注意的事情就是 CommandPrefix 元素。 将该元素考虑作为用户可以说出以便调用您的应用程序的别名。 这在您的应用程序名称具有非标准的拼写或无法发音的字符(例如 Mag1c 或 gr00ve)时将很有用。 请记住,这个词汇或短语必须是语言识别引擎可以识别的,并且还必须在发音上不同于 Windows Phone 内置关键字。

您会注意到,存在 Example 元素作为 CommandSet 元素和 Command 元素的子集。 CommandSet 下的 Example 是针对您的应用程序的一个一般示例,将显示系统帮助“What can I say?”(我可以说什么?)。 屏幕,如图 2. 中所示。相反,Command 下的 Example 元素是特定于该 Command 的。 此 Example 显示一个系统帮助页(见图 3),当用户在图2 中所示的帮助页上点击应用程序名称时将显示该系统帮助页。

Help Page Showing Voice Command Examples for Installed Apps
图 2 为已安装的应用程序显示语音命令示例的帮助页

Example Page for Magic Memo Voice Commands
图 3 Magic Memo 语音命令的示例页

说到这里,CommandSet 内的每个 Command 子元素都对应于启动后要在应用程序中执行的一个操作。 在一个 Command 中可能有多个 ListenFor 元素,但它们全都应该以不同的方式告诉应用程序执行它们是其子集的操作(命令)。

还要注意,ListenFor 元素中的文本具有两个特殊结构。 用方括号将文本括起来意味着该文本是可选的,也就是说,在识别用户的语句时可以使用或不使用括起来的文本。 花括号包含引用 PhraseList 元素的标签。 在 图 1 的美式英语示例中,“showOne”命令下的第一个 ListenFor 具有标签 {num},它引用其下的短语列表。 您可以将此视为可用被引用列表中的任何短语填充的位置,在这个例子中是数字。

在用户的语句中识别命令时将发生什么? 手机的全局语音识别器将在相应 Command 下的 Navigate 元素的 Target 属性中指定的页上启动该应用程序,在后面的步骤 3 中将对此进行说明。 但首先,我将论述步骤 2。

启用语音命令

在您的安装包中包括了 VCD 文件后,步骤 2 是注册该文件,以便 Windows Phone 8 可以在系统语法中包含该应用程序的命令。 您通过在 VoiceCommandService 类上调用静态方法 InstallCommandSetsFromFileAsync 来进行注册,如图 4 中所示。 大多数应用程序都将在首次运行时进行此调用,当然,也可以在任何时间进行此调用。 实现 VoiceCommandService 具有足够的智能程度,如果在文件中没有更改,这一智能足以对后续调用不执行任何操作,因此,不用担心每次启动应用程序时都会调用它。

图 4 从应用程序内初始化 VCD 文件

using Windows.Phone.Speech.VoiceCommands;
// ...
// Standard boilerplate method in the App class in App.xaml.cs
private async void Application_Launching(object sender, 
  LaunchingEventArgs e)
{
  try // try block recommended to detect compilation errors in VCD file
  {
    await VoiceCommandService.InstallCommandSetsFromFileAsync(
      new Uri("ms-appx:///MagicMemoVCD.xml"));
  }
  catch (Exception ex)
  {
    // Handle exception
  }
}

正如方法名称 InstallCommandSetsFromFileAsync 所表达的意思,该 VCD 文件中的操作单元是一个 CommandSet 元素,而非该文件本身。 对此方法的调用检查并验证在该文件中包含的所有命令集,但它仅安装其 xml:lang 属性与全局语音引擎的这个属性完全匹配的命令集。 如果用户将全局识别语言切换为与您的 VCD 中不同 CommandSet 的 xml:lang 匹配的语言,将会加载并激活该 CommandSet。

处理语音命令

现在我将论述步骤 3。 在全局语音识别器识别来自您的应用程序的命令前缀和命令后,它将在 Navigate 元素的 Target 属性中指定的页上启动该应用程序;并且如果未指定 Target,则使用您的默认任务目标(对于 Silverlight 应用程序通常为 MainPage.xaml)。 它还追加到 Command 名称和 PhraseList 值的查询字符串键/值对。 例如,如果识别的短语为“Magic Memo show memo number three”,则查询字符串可能类似如下形式(实际字符串可能因实现方式或版本而异):

"/ViewMemos.xaml?voiceCommandName=show&num=3&
reco=show%20memo%20number%20three"

幸运的是,您不必对查询字符串进行分析和自己研究参数,在 NavigationContext 对象的 QueryString 集合中提供了它们。 该应用程序可以使用此数据来确定它是否已由语音命令启动 — 并且如果已启动,则相应地处理该命令(例如,在该页的 Loaded 处理程序中)。 图 5 显示来自 Magic Memo 应用程序的针对 ViewMemos.xaml 页的示例。

图 5 在应用程序中处理语音命令

// Takes appropriate action if the application was launched by voice command.
private void ViewMemosPage_Loaded(object sender, RoutedEventArgs e)
{
  // Other code omitted
  // Handle the case where the page was launched by Voice Command
  if (this.NavigationContext.QueryString != null
    && this.NavigationContext.QueryString.ContainsKey("voiceCommandName"))
  {
    // Page was launched by Voice Command
    string commandName =
      NavigationContext.QueryString["voiceCommandName"];
    string spokenNumber = "";
    if (commandName == "showOne" &&
      this.NavigationContext.QueryString.TryGetValue("num", 
        out spokenNumber))
    {
      // Command was "Show memo number 'num'"
      int index = -1;
      if (int.TryParse(spokenNumber, out index) &&
        index <= memoList.Count && index > 0)
      { // Display the specified memo
        this.Dispatcher.BeginInvoke(delegate
          { MessageBox.Show(String.Format(
          "Memo {0}: \"{1}\"", index, memoList[index - 1])); });
      }
    }
    // Note: no need for an "else" block because if launched by another VoiceCommand
    // then commandName="showAll" and page is shown
  }
}

因为有多种方法可以导航到任意页,所以,图 5 中的代码首先检查在查询字符串中是否存在 voiceCommandName 键,以便确定用户是否通过语音命令启动了该应用程序。 如果启动了应用程序,它将验证命令名称并且获取 PhraseList 参数 num 的值,该值为用户希望看到的备忘录的数目。 该页只有两个语音命令并且处理十分简单,但是,可由许多语音命令启动的页面将会使用类似 commandName 上的开关块之类的内容决定要执行的操作。

此示例中的 PhraseList 也很简单;它仅是一系列数字,每个数字对应于每个存储的备忘录。 您可以设想更复杂的情形,但是,要求动态填充(例如,通过网站上的数据)的短语列表。 前面提到的可选步骤 4 说明对于这些情形如何实现 PhraseList。 接下来我将对此进行论述。

从您的应用程序更新短语列表

您可能已注意到图 1 中的 VCD 文件有问题。 在该 VCD 中以静态方式定义的“num”PhraseList 支持识别最多三项,但最终在应用程序的独立存储中很可能存储超过三个备忘录。 对于短语列表随时间更改的用例,可以从应用程序内动态更新短语列表,如图 6 中所示。 这对于需要识别动态列表(例如下载的电影、喜爱的餐馆或电话当前位置附近的关注点)的应用程序特别有用。

图 6 动态更新已安装的短语列表

// Updates the "num" PhraseList to have the same number of
// entries as the number of saved memos; this supports
// "Magic Memo show memo 5" if there are five or more memos saved
private async void UpdateNumberPhraseList(string phraseList,
  int newLimit, string commandSetName)
{
  // Helper function that sets string array to {"1", "2", etc.}
  List<string> positiveIntegers =
    Utilities.GetStringListOfPositiveIntegers(Math.Max(1, newLimit));
  try
  {
    VoiceCommandSet vcs = null;
    if (VoiceCommandService.InstalledCommandSets.TryGetValue(
      commandSetName, out vcs))
    {
      // Update "num" phrase list to the new numbers
      await vcs.UpdatePhraseListAsync(phraseList, positiveIntegers);
    }
  }
  catch (Exception ex)
  {
    this.Dispatcher.BeginInvoke(delegate
      { MessageBox.Show("Exception in UpdateNumberPhraseList " 
        + ex.Message); }
    );
  }
}

尽管 Magic Memo 应用程序没有演示,但动态更新的短语列表非常适合于在用户代理中进行更新,因为更新可在后台发生,甚至是在应用程序未运行时进行。

至此您已实现了您的目的: 采用四个步骤在您的应用程序中实现语音命令。 用 Magic Memo 示例应用程序进行尝试。 请记住,您需要正常运行它一次以便加载 VCD 文件,但加载后,您就可以说出下面这样的话来启动应用程序并且转到所需页面和执行命令:

  • Magic Memo,输入新备忘录
  • Magic Memo,显示所有备忘录
  • Magic Memo,显示第四条备忘录

接下来: 应用程序内对话

按照我在本文中所论述的内容实现语音命令是让您的用户与您在 Windows Phone 8 上的应用程序进行交互的第一步,就像对于短信、查找和呼叫之类的内置应用程序一样。

第二步是提供应用程序内对话,在对话中,用户在您的应用程序启动后向它说话,以便录制文本或执行命令,以及将音频反馈以说出的文本形式接收。 我将在部分 2 中对此主题进行深入探讨,因此请继续关注。

F Avery Bishop 在软件开发方面具有超过 20 年的工作经验,其中 12 年是在 Microsoft 度过的,在 Microsoft 他担任语音平台的项目经理。 他发表了许多与应用程序中的自然语言支持有关的文章,包括复杂脚本支持、多语言应用程序和语音识别之类的主题。

衷心感谢以下技术专家对本文的审阅: Robert Brown、Victor Chang、Jay Waltmunson 和 Travis Wilson