Windows 10中引入了Cortana语音助手,同时开放了一些新的API以供开发者接入。
最近在做Cortana接入,实现一些简单的交互操作。期间遇到了一些问题,目前关于UWP的资料也比较少,还好最后问题都解决了。
目前Windows 10中Cortana支持前台集成(跳转到应用)和后台集成(在Cortana中完成任务),下面将针对两种模式分别介绍。
1、VCD文件介绍
VCD文件是一个命令配置文件,可以在VCD文件中定义命令等信息。VCD文件有以下几个元素组成:
VoiceCommands:VoiceCommands的xmlns属性值必须为 http://schemas.microsoft.com/voicecommands/1.2,WP的是1.0 WP8.1的是1.1(没记错的话),VoiceCommands有1-15个CommandSet,对应着不同的语言。
CommandSet:是针对不同语言定义的一组命令,xml:lang=“zh-cn”标识这组命令对应着中文。
CommandPrefix:是CommandSet的子节点,定义了命令的前缀,通常命令组成是以 CommandPrefix开头+Command来实现命令。
Command:定义了单个命令。
ListenFor:表示需要接听的命令,如 搜索{scenery} 可以识别所有 CommandPrefix+搜索+景区 的命令。{scenery}可以在PhraseTopic中定义类型。可以同时定义多组ListenFor,如:
Feedback:表示Cortana识别命令后,在执行应用的代码之前给用户的一个反馈。
VoiceCommandService、Navigate:两个可选,但是必须有一个。Navigate用于启动App来执行操作(前台集成),VoiceCommandService用于在Cortana中完成任务(后台集成)
PhraseTopic:是CommandSet的子节点,有点类似占位符的意思,可以定义PhraseTopic的场景来Subject来提高识别率。比如 我要去{city}
更多关于VCD文件的介绍可以查看MSDN中的文档:https://msdn.microsoft.com/zh-cn/library/windows/apps/xaml/dn706593.aspx
2、后台集成
后台集成可以不打开应用,直接在Cortana中完成任务。下面将针对实例场景来说明如何后台集成Cortana。
首先新建一个UWP应用CortanaDemo,新增一个Windows Runtime Component工程CortanaService。新建一个CortanaCommandService,继承IBackgroundTask接口。在CortanaDemo中引用CortanaService,打开Package.appxmanifest文件,在Application节点添加如下节点:
1 <Extensions> 2 <uap:Extension Category="windows.appService" EntryPoint="CortanaDemo.CortanaService.CortanaCommandService"> 3 <uap:AppService Name="CortanaCommandService" /> 4 uap:Extension> 5 <uap:Extension Category="windows.personalAssistantLaunch"/> 6 Extensions>
同时在CortanaDemo中新增一个VCD文件CortanaCommand.xml ,定义一个命令:
xml version="1.0" encoding="utf-8" ?> <VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.2"> <CommandSet xml:lang="zh-cn" Name="AdventureWorksCommandSet_zh-cn"> <CommandPrefix>我要玩CommandPrefix> <Example>我要玩Example> <Command Name="ScenerySearch"> <Example>搜索 苏州乐园 Example> <ListenFor>搜索{destination}ListenFor> <ListenFor>我要去{destination}ListenFor> <ListenFor>{destination}的门票ListenFor> <ListenFor>{destination}ListenFor> <ListenFor>查找{destination}ListenFor> <Feedback> 正在搜索{destination}... Feedback> <VoiceCommandService Target="TCTVoiceCommandService"/> Command> <PhraseTopic Label="destination" Scenario="Natural Language"> <Subject>City/StateSubject> <Subject>City/StateSubject> PhraseTopic> <PhraseTopic Label="from" Scenario="Natural Language"> <Subject>City/StateSubject> PhraseTopic> <PhraseTopic Label="to" Scenario="Natural Language"> <Subject>City/StateSubject> PhraseTopic> <PhraseTopic Label="date" Scenario="Natural Language"> <Subject>Date/TimeSubject> PhraseTopic> CommandSet> VoiceCommands>
同时在App.xaml.cs中添加如下代码:
var vcdStorageFile = await Package.Current.InstalledLocation.GetFileAsync(@"CortanaCommand.xml"); await Windows.ApplicationModel.VoiceCommands.VoiceCommandDefinitionManager.InstallCommandDefinitionsFromStorageFileAsync(vcdStorageFile);
到此,项目就搭建完成了。下面来实现IBackgroundTask接口。
public async void Run(IBackgroundTaskInstance taskInstance) {
//异步任务需要获取Deferral。 serviceDeferral = taskInstance.GetDeferral(); taskInstance.Canceled += OnTaskCanceled; var triggerDetails = taskInstance.TriggerDetails as AppServiceTriggerDetails; if (triggerDetails != null && triggerDetails.Name == "CortanaCommandService") { try { voiceServiceConnection = VoiceCommandServiceConnection.FromAppServiceTriggerDetails( triggerDetails); voiceServiceConnection.VoiceCommandCompleted += OnVoiceCommandCompleted; VoiceCommand voiceCommand = await voiceServiceConnection.GetVoiceCommandAsync(); // perform the appropriate command. switch (voiceCommand.CommandName) { case "ScenerySearch": var destination = voiceCommand.Properties["destination"][0]; await SearchSceneryByKey(destination); break; default: LaunchAppInForeground(); break; } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Handling Voice Command failed " + ex.ToString()); } } }
private async Task SearchSceneryByKey(string destination) { #region 查询 //通过destination来从网络查询数据..... #endregion var destinationsContentTiles = new List(); foreach (var scenery in result.Item2) { destinationsContentTiles.Add(new VoiceCommandContentTile { Title = scenery.sceneryName, TextLine1 = "¥" + scenery.tcPrice, TextLine2 = scenery.address, ContentTileType = VoiceCommandContentTileType.TitleWithText }); } var userMessage = new VoiceCommandUserMessage { DisplayMessage = string.Format("{0}有关的景点", destination), SpokenMessage = string.Format("找到了{0}条信息,请问您想查看哪一条?", result.Item2.Count) }; var repeat = new VoiceCommandUserMessage { DisplayMessage = "请再说一遍", SpokenMessage = "不好意思,没听清楚" }; var response = VoiceCommandResponse.CreateResponseForPrompt(userMessage, repeat, destinationsContentTiles); var resultd = await voiceServiceConnection.RequestDisambiguationAsync(response); var selectedScenery = resultd.SelectedItem.AppContext as resScenerysModel; SceneryVMLocator.SceneryVM.CurrentScenery = new resScenerysModel { sceneryId = selectedScenery.sceneryId }; userMessage = new VoiceCommandUserMessage { DisplayMessage = "正在显示景点详情...", SpokenMessage = "正在显示景点详情..." }; response = VoiceCommandResponse.CreateResponse(userMessage); response.AppLaunchArgument = InternalUrlBuilder.Build(ProductType.Scenery, InternalUrlBuilder.Action.details, selectedScenery.sceneryId); await voiceServiceConnection.RequestAppLaunchAsync(response); }
这点代码有以下功能:
1、获取命令类型
2、查询网络数据
3、展示并询问用户需要查看哪一条
4、启动应用显示详情
至此基本完成了Cortana后台集成。
VoiceCommandServiceConnection有以下几个方法:
1、GetVoiceCommandAsync:获取命令
2、ReportFailureAsync:返回失败
3、ReportProgressAsync:向用户报告进度,如果需要长耗时操作,需要向用户返回当前的操作信息。
4、ReportSuccessAsync:返回操作成功而不需要接收返回。
5、RequestAppLaunchAsync:从Cortana中启动应用。
6、RequestConfirmationAsync:向用户发送Yes/No请求
7、RequestDisambiguationAsync:当含有多条数据时,供用户选择,如返回若干条景区信息,询问用户需要查看哪一条。
不知道大家有没有发现代码中有两处被加粗加大了。这两处是我在开发时遇到的坑。
1、ContentTileType = VoiceCommandContentTileType.TitleWithText。
在添加数据时,可以展示多种类型的列表,如仅标题、图片等,具体信息可以查看VoiceCommandContentTileType。
在开发的时候,只需要展示一些基本的景区信息,而没有展示景区图片,所以最初只定义了
new VoiceCommandContentTile { Title = scenery.sceneryName, TextLine1 = "¥" + scenery.tcPrice, TextLine2 = scenery.address}
在部署操作的时候,Cortana经常返回失败,偶尔会成功展示几次信息,而去没有任何报错信息。当时对着Cortana喊了一下午,各种调试都不行(旁边的iOS开发已经默默的戴上了耳机)。后来设置了ContentTileType = VoiceCommandContentTileType.TitleWithText才成功,具体原因还不清楚,不知道是不是ContentTileType默认值是带icon的,导致image找不到才提示错误。
2、
当用户选择要查看哪个景区的时候,此时下单流程比较麻烦,需要回到客户端中进行操作,此时可以调用RequestAppLaunchAsync来启动客户端,在App.xaml.cs里override OnActivated方法来接收启动参数
protected async override void OnActivated(IActivatedEventArgs args) { base.OnActivated(args); shell = Window.Current.Content as AppShell; // Do not repeat app initialization when the Window already has content, // just ensure that the window is active if (shell == null) { // Create a AppShell to act as the navigation context and navigate to the first page shell = new AppShell(); // Set the default language shell.Language = Windows.Globalization.ApplicationLanguages.Languages[0]; shell.AppFrame.NavigationFailed += OnNavigationFailed; } // Place our app shell in the current Window Window.Current.Content = shell; //读写文件一般比较快,几乎不会影响启动性能 CurrentCity = await IsolatedStorageHelper.Instance.GetLastCity(); Window.Current.Activate(); switch (args.Kind) { case ActivationKind.VoiceCommand: { break; } case ActivationKind.Protocol: { var command = args as ProtocolActivatedEventArgs; Windows.Foundation.WwwFormUrlDecoder decoder = new Windows.Foundation.WwwFormUrlDecoder(command.Uri.Query); var destination = decoder.GetFirstValueByName("LaunchContext"); new MessageDialog(destination).ShowAsync(); InternalJumper.initNoticeUrl(destination); break; } } }
RequestAppLaunchAsync的启动类型是Protocol,可以把args强制转换为ProtocolActivatedEventArgs来取到Uri参数。
那么问题来了,在开发过程中,每次调用RequestAppLaunchAsync来启动客户端的时候,应用都会闪退,各种google,bing也搜不到资料,直到与微软的demo一行行代码对比后才发现需要添加一个权限才能实现启动
重要的事情要说三遍、三遍、三遍。
添加后应用完美启动。
备注:
VoiceCommandContentTile的Title要有区分性,调用RequestDisambiguationAsync的时候会报错。
2、前台启动
待续...