通过 Cortana 使用语音命令激活前台应用

警告

自 Windows 10 2020 年 5 月更新(版本 2004,codename“20H1”)起,不再支持此功能。

请参阅 Microsoft 365 中的 Cortana,了解 Cortana 如何改变现代生产力体验。

除了在 Cortana 中使用语音命令访问系统功能外,还可以使用应用中的特性和功能来扩展 Cortana。 使用语音命令可以在前台激活应用,并在应用内部执行操作或命令。

当应用在前台处理语音命令时,它会获取焦点并关闭 Cortana。 如果需要,你可以后台任务的形式激活应用并执行命令。 在这种情况下,Cortana 将保留焦点,应用将通过 Cortana 画布和 Cortana 语音返回所有反馈与结果。

需要额外上下文或用户输入(例如将消息发送给特定联系人)的命令最好是通过前台应用处理,而基本命令(如列出即将到来的行程)可以通过后台应用使用 Cortana 来处理。

若要使用语音命令在后台激活应用,请参阅在 Cortana 中使用语音命令激活后台应用

注意

语音命令是在语音命令定义 (VCD) 文件中定义且具有特定意图的单个言语,通过 Cortana 指向一个已安装的应用。

VCD 文件定义了一个或多个语音命令,每个命令都有一个独特的意图。

语音命令定义在复杂性方面可能各不相同。 这些定义可以支持任何内容,从单一的受约束的言语到更灵活的自然语言言语的集合,所有这些都表示相同的意图。

为了演示前台应用的功能,我们将使用 Cortana 语音命令示例中名为“Adventure Works”的行程规划和管理应用。

若要在不使用 Cortana 的情况下创建新的 Adventure Works 行程,用户需启动该应用并导航到“新建行程”页。 若要查看现有行程,用户需启动该应用,导航到“即将出发的行程”页,然后选择该行程。

通过 Cortana 使用语音命令,用户可以改为说出“Adventure Works 添加行程”或“在 Adventure Works 上添加行程”来启动该应用并导航到“新建行程”页。 而说出“Adventure Works,请展示我的伦敦之旅”会启动该应用并导航到此处显示的“行程”详细信息页。

Screenshot of Cortana launching foreground app

下面是添加语音命令功能以及使用语音或键盘输入将 Cortana 与应用集成的基本步骤:

  1. 创建一个 VCD 文件。 此文件是一个 XML 文档,它定义了用户在激活你的应用时可以说出的所有用于启动操作或调用命令的语音命令。 请参阅 VCD 元素和属性 v1.2
  2. 启动应用时,在 VCD 文件中注册命令集。
  3. 处理语音命令激活、应用内导航和命令执行。

提示

必备条件

如果你还不熟悉通用 Windows 平台 (UWP) 应用开发,请查看这些主题来熟悉此处讨论的技术。

用户体验指南

有关如何将应用与 Cortana 和语音交互集成的信息,请参阅 Cortana 设计指导原则,其中提供了有关设计有用且有吸引力的支持语音的应用的有用提示。

在 Visual Studio 中使用项目创建新的解决方案

  1. 启动 Microsoft Visual Studio 2015。

    此时将显示 Visual Studio 2015 开始页。

  2. 在“文件”菜单中,选择“新建”>“项目”。

    将显示“新建项目”对话框。 在该对话框的左侧窗格中,可以选择要显示的模板类型。

  3. 在左窗格中,展开“已安装>模板>”Visual C# > Windows,然后选择通用模板组。 该对话框的中心窗格将显示通用 Windows 平台 (UWP) 应用的项目模板列表。

  4. 在中心窗格中,选择“空白应用(通用 Windows)”模板。

    “空白应用”模板将创建一个精简的 UWP 应用,该应用能够编译和运行,但不包含用户界面控件或数据。 在本教程中,你将在应用中添加控件。

  5. 在“名称”文本框中,键入你的项目名称。 对于本示例,我们将使用“AdventureWorks”。

  6. 单击“确定”以创建该项目。

    Microsoft Visual Studio 将创建该项目,并在“解决方案资源管理器”中显示该项目。

将图像资产添加到项目并在应用清单中指定这些资产

UWP 应用可以根据特定的设置和设备功能(高对比度、有效像素、区域设置等)自动选择最合适的图像。 你只需提供图像并确保在应用项目中为不同的资源版本使用适当的命名约定和文件夹组织。 如果未提供建议资源版本,则辅助功能、本地化和图像质量可能因用户的首选项、功能、设备类型和位置而异。

有关高对比度和缩放比例的图像资源的更多详细信息,请参阅磁贴和图标资产准则

使用限定符命名资源。 资源限定符是文件夹和文件名修饰符,用于标识应使用特定版本资源的上下文。

标准命名约定是 foldername/qualifiername-value[_qualifiername-value]/filename.qualifiername-value[_qualifiername-value].ext。 例如 images/logo.scale-100_contrast-white.png,只需在代码中使用根文件夹和文件名即可引用它:images/logo.png。 请参阅如何使用限定符命名资源

我们建议在字符串资源文件(例如 en-US\resources.resw)上标记默认语言,在图像上标记默认比例因子(例如 logo.scale-100.png),即使你当前不打算提供本地化或多分辨率资源。 但是,我们建议至少提供 100、200 和 400 比例因子的资产。

重要

Cortana 画布标题区域中使用的应用图标是“Package.appxmanifest”文件中指定的 Square44x44Logo 图标。

创建 VCD 文件

  1. 在Visual Studio中,右键单击主项目名称,选择“添加新>项”。 添加一个 XML 文件。
  2. 键入 VCD 文件的名称(在本例中为“AdventureWorksCommands.xml”),然后单击“添加”。
  3. 在“解决方案资源管理器”中,选择该 VCD 文件。
  4. 在“属性”窗口中,将“生成操作”设置为“内容”,然后将“复制到输出目录”设置“如果较新则复制”。

编辑 VCD 文件

添加一个 VoiceCommands 元素,其中包含指向 https://schemas.microsoft.com/voicecommands/1.2 的 xmlns 属性。

  1. 对于应用支持的每种语言,请创建一个包含应用支持的语音命令的 CommandSet 元素。

    可以声明多个 CommandSet 元素,其中每个元素具有不同的 xml:lang 属性,因此你的应用可以在不同的市场中使用。 例如,在美国使用的应用可以使用英语的 CommandSet 和西班牙语的 CommandSet

    注意

    若要激活应用并使用语音命令启动操作,应用必须注册一个 VCD 文件,其中包含一个语言与设备用户所选语音语言相匹配的 CommandSet 元素。 语音语言位于设置>系统>语音>语言中。

  2. 为你要支持的每个命令添加一个 Command 元素。 在 VCD 文件中声明的每个 Command 必须包含以下信息:

    • 一个 AppName 属性,在运行时,应用程序将使用该属性来识别语音命令。
    • 一个 Example 元素,其中包含描述用户如何调用命令的短语。 当用户说出“我可以说什么?”、“帮助”或点击“查看更多”时,Cortana 会显示此示例。
    • 一个 ListenFor 元素,其中包含已由应用识别为命令的单词或短语。 每个 ListenFor 元素可以包含对一个或多个 PhraseList 元素的引用,这些元素包含与命令相关的特定单词。

注意

无法以编程方式修改 ListenFor 元素。 但是,可以编程方式修改与 ListenFor 元素关联的 PhraseList 元素。 在运行时,应用程序应根据用户使用应用时生成的数据集修改 PhraseList 元素的内容。 请参阅动态修改 Cortana VCD 短语列表

一个 Feedback 元素,其中包含 Cortana 在应用程序启动时显示和朗读的文本。

Navigate 元素指示语音命令将在前台激活应用。 在此示例中,showTripToDestination 命令是前台任务。

VoiceCommandService 元素指示语音命令将在后台激活应用。 此元素的 Target 属性值应与 package.appxmanifest 文件中 uap:AppService 元素的 Name 属性值相匹配。 在此示例中,whenIsTripToDestinationcancelTripToDestination 命令是后台任务,将应用服务的名称指定为“AdventureWorksVoiceCommandService”。

有关更多详细信息,请参阅 VCD 元素和属性 v1.2 参考

下面是为 Adventure Works 应用定义美国英语语音命令的 VCD 文件的一部分。

<?xml version="1.0" encoding="utf-8" ?>
<VoiceCommands xmlns="https://schemas.microsoft.com/voicecommands/1.2">
  <CommandSet xml:lang="en-us" Name="AdventureWorksCommandSet_en-us">
    <AppName> Adventure Works </AppName>
    <Example> Show trip to London </Example>

    <Command Name="showTripToDestination">
      <Example> Show trip to London </Example>
      <ListenFor RequireAppName="BeforeOrAfterPhrase"> show [my] trip to {destination} </ListenFor>
      <ListenFor RequireAppName="ExplicitlySpecified"> show [my] {builtin:AppName} trip to {destination} </ListenFor>
      <Feedback> Showing trip to {destination} </Feedback>
      <Navigate />
    </Command>

    <Command Name="whenIsTripToDestination">
      <Example> When is my trip to Las Vegas?</Example>
      <ListenFor RequireAppName="BeforeOrAfterPhrase"> when is [my] trip to {destination}</ListenFor>
      <ListenFor RequireAppName="ExplicitlySpecified"> when is [my] {builtin:AppName} trip to {destination} </ListenFor>
      <Feedback> Looking for trip to {destination}</Feedback>
      <VoiceCommandService Target="AdventureWorksVoiceCommandService"/>
    </Command>
    
    <Command Name="cancelTripToDestination">
      <Example> Cancel my trip to Las Vegas </Example>
      <ListenFor RequireAppName="BeforeOrAfterPhrase"> cancel [my] trip to {destination}</ListenFor>
      <ListenFor RequireAppName="ExplicitlySpecified"> cancel [my] {builtin:AppName} trip to {destination} </ListenFor>
      <Feedback> Cancelling trip to {destination}</Feedback>
      <VoiceCommandService Target="AdventureWorksVoiceCommandService"/>
    </Command>

    <PhraseList Label="destination">
      <Item>London</Item>
      <Item>Las Vegas</Item>
      <Item>Melbourne</Item>
      <Item>Yosemite National Park</Item>
    </PhraseList>
  </CommandSet>

安装 VCD 命令

必须运行应用一次以安装 VCD。

注意

语音命令数据不会在应用安装中保留。 为确保应用的语音命令数据保持不变,请考虑在每次启动或激活应用时初始化 VCD 文件,或维护一个指示当前是否安装了 VCD 的设置。

在“app.xaml.cs”文件中:

  1. 添加以下 using 指令:

    using Windows.Storage;
    
  2. 使用 async 修饰符标记“OnLaunched”方法。

    protected async override void OnLaunched(LaunchActivatedEventArgs e)
    
  3. OnLaunched 处理程序中调用 InstallCommandDefinitionsFromStorageFileAsync,以注册系统应识别的语音命令。

在 Adventure Works 示例中,我们首先定义一个 StorageFile 对象。

然后我们调用 GetFileAsync,以使用“AdventureWorksCommands.xml”文件将其初始化。

然后将此 StorageFile 对象传递给 InstallCommandDefinitionsFromStorageFileAsync

try
{
  // Install the main VCD. 
  StorageFile vcdStorageFile = 
  await Package.Current.InstalledLocation.GetFileAsync(
  @"AdventureWorksCommands.xml");

  await Windows.ApplicationModel.VoiceCommands.VoiceCommandDefinitionManager.
InstallCommandDefinitionsFromStorageFileAsync(vcdStorageFile);

  // Update phrase list.
  ViewModel.ViewModelLocator locator = App.Current.Resources["ViewModelLocator"] as ViewModel.ViewModelLocator;
  if(locator != null)
  {
     await locator.TripViewModel.UpdateDestinationPhraseList();
  }
}
catch (Exception ex)
{
  System.Diagnostics.Debug.WriteLine("Installing Voice Commands Failed: " + ex.ToString());
}

处理激活和执行语音命令

指定应用如何响应后续语音命令激活(在至少启动应用一次并安装语音命令集之后)。

  1. 确认应用已通过语音命令激活。

    重写 Application.OnActivated 事件并检查 IActivatedEventArgs.Kind 是否为 VoiceCommand

  2. 确定命令的名称和所说的内容。

    IActivatedEventArgs 获取对 VoiceCommandActivatedEventArgs 对象的引用,并查询 SpeechRecognitionResult 对象的 Result 属性。

    若要确定用户所说的内容,请检查 Text 的值或 SpeechRecognitionSemanticInterpretation 字典中已识别的短语的语义属性。

  3. 在应用中采取适当的操作,例如导航到所需的页面。

对于本示例,我们将重新引用“步骤 3:编辑 VCD 文件”中的 VCD。

获取语音命令的语音识别结果后,便已从 RulePath 数组中的第一个值获取了命令名称。 由于 VCD 文件定义了多个可能的语音命令,因此我们需要将该值与 VCD 中的命令名称进行比较,并采取适当的操作。

应用程序执行的最常见操作是导航到内容与语音命令上下文相关的页面。 对于本示例,我们将导航到“TripPage”页面并传入语音命令的值、命令的输入方式以及识别的“目标”短语(如果适用)。 或者,在导航到该页面时,应用可以向 SpeechRecognitionResult 发送导航参数。

可以使用 commandMode 键从 SpeechRecognitionSemanticInterpretation.Properties 字典中确定启动应用的语音命令是否实际被说出,或者是否以文本形式键入。 该键的值是“voice”或“text”。 如果该键的值是“voice”,请考虑在应用中使用语音合成 (Windows.Media.SpeechSynthesis) 为用户提供语音反馈。

使用 SpeechRecognitionSemanticInterpretation.Properties 确定在 ListenFor 元素的 PhraseList 或 PhraseTopic 约束中说出的内容。 字典键是 PhraseList 或 PhraseTopic 元素的 Label 属性的值。 此处演示了如何访问 {destination} 短语的值。

/// <summary>
/// Entry point for an application activated by some means other than normal launching. 
/// This includes voice commands, URI, share target from another app, and so on. 
/// 
/// NOTE:
/// A previous version of the VCD file might remain in place 
/// if you modify it and update the app through the store. 
/// Activations might include commands from older versions of your VCD. 
/// Try to handle these commands gracefully.
/// </summary>
/// <param name="args">Details about the activation method.</param>
protected override void OnActivated(IActivatedEventArgs args)
{
    base.OnActivated(args);

    Type navigationToPageType;
    ViewModel.TripVoiceCommand? navigationCommand = null;

    // Voice command activation.
    if (args.Kind == ActivationKind.VoiceCommand)
    {
        // Event args can represent many different activation types. 
        // Cast it so we can get the parameters we care about out.
        var commandArgs = args as VoiceCommandActivatedEventArgs;

        Windows.Media.SpeechRecognition.SpeechRecognitionResult speechRecognitionResult = commandArgs.Result;

        // Get the name of the voice command and the text spoken. 
        // See VoiceCommands.xml for supported voice commands.
        string voiceCommandName = speechRecognitionResult.RulePath[0];
        string textSpoken = speechRecognitionResult.Text;

        // commandMode indicates whether the command was entered using speech or text.
        // Apps should respect text mode by providing silent (text) feedback.
        string commandMode = this.SemanticInterpretation("commandMode", speechRecognitionResult);
        
        switch (voiceCommandName)
        {
            case "showTripToDestination":
                // Access the value of {destination} in the voice command.
                string destination = this.SemanticInterpretation("destination", speechRecognitionResult);

                // Create a navigation command object to pass to the page. 
                navigationCommand = new ViewModel.TripVoiceCommand(
                    voiceCommandName,
                    commandMode,
                    textSpoken,
                    destination);

                // Set the page to navigate to for this voice command.
                navigationToPageType = typeof(View.TripDetails);
                break;
            default:
                // If we can't determine what page to launch, go to the default entry point.
                navigationToPageType = typeof(View.TripListView);
                break;
        }
    }
    // Protocol activation occurs when a card is clicked within Cortana (using a background task).
    else if (args.Kind == ActivationKind.Protocol)
    {
        // Extract the launch context. In this case, we're just using the destination from the phrase set (passed
        // along in the background task inside Cortana), which makes no attempt to be unique. A unique id or 
        // identifier is ideal for more complex scenarios. We let the destination page check if the 
        // destination trip still exists, and navigate back to the trip list if it doesn't.
        var commandArgs = args as ProtocolActivatedEventArgs;
        Windows.Foundation.WwwFormUrlDecoder decoder = new Windows.Foundation.WwwFormUrlDecoder(commandArgs.Uri.Query);
        var destination = decoder.GetFirstValueByName("LaunchContext");

        navigationCommand = new ViewModel.TripVoiceCommand(
                                "protocolLaunch",
                                "text",
                                "destination",
                                destination);

        navigationToPageType = typeof(View.TripDetails);
    }
    else
    {
        // If we were launched via any other mechanism, fall back to the main page view.
        // Otherwise, we'll hang at a splash screen.
        navigationToPageType = typeof(View.TripListView);
    }

    // Repeat the same basic initialization as OnLaunched() above, taking into account whether
    // or not the app is already active.
    Frame rootFrame = Window.Current.Content as Frame;

    // Do not repeat app initialization when the Window already has content,
    // just ensure that the window is active.
    if (rootFrame == null)
    {
        // Create a frame to act as the navigation context and navigate to the first page.
        rootFrame = new Frame();
        App.NavigationService = new NavigationService(rootFrame);

        rootFrame.NavigationFailed += OnNavigationFailed;

        // Place the frame in the current window.
        Window.Current.Content = rootFrame;
    }

    // Since we're expecting to always show a details page, navigate even if 
    // a content frame is in place (unlike OnLaunched).
    // Navigate to either the main trip list page, or if a valid voice command
    // was provided, to the details page for that trip.
    rootFrame.Navigate(navigationToPageType, navigationCommand);

    // Ensure the current window is active
    Window.Current.Activate();
}

/// <summary>
/// Returns the semantic interpretation of a speech result. 
/// Returns null if there is no interpretation for that key.
/// </summary>
/// <param name="interpretationKey">The interpretation key.</param>
/// <param name="speechRecognitionResult">The speech recognition result to get the semantic interpretation from.</param>
/// <returns></returns>
private string SemanticInterpretation(string interpretationKey, SpeechRecognitionResult speechRecognitionResult)
{
  return speechRecognitionResult.SemanticInterpretation.Properties[interpretationKey].FirstOrDefault();
}