UWP集成Cortana
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,如:
<ListenFor>搜索{scenery}</ListenFor>
<ListenFor>我要去{scenery}</ListenFor>
<ListenFor>{scenery}的门票</ListenFor>
Feedback:表示Cortana识别命令后,在执行应用的代码之前给用户的一个反馈。
VoiceCommandService、Navigate:两个可选,但是必须有一个。Navigate用于启动App来执行操作(前台集成),VoiceCommandService用于在Cortana中完成任务(后台集成)
PhraseTopic:是CommandSet的子节点,有点类似占位符的意思,可以定义PhraseTopic的场景来Subject来提高识别率。比如 我要去{city}
<PhraseTopic Label="city" Scenario="Natural Language">
<Subject>City/State</Subject>
</PhraseTopic>
更多关于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/State</Subject>
<Subject>City/State</Subject>
</PhraseTopic>
<PhraseTopic Label="from" Scenario="Natural Language">
<Subject>City/State</Subject>
</PhraseTopic>
<PhraseTopic Label="to" Scenario="Natural Language">
<Subject>City/State</Subject>
</PhraseTopic>
<PhraseTopic Label="date" Scenario="Natural Language">
<Subject>Date/Time</Subject>
</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<VoiceCommandContentTile>();
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、<uap:Extension Category="windows.personalAssistantLaunch"/>
当用户选择要查看哪个景区的时候,此时下单流程比较麻烦,需要回到客户端中进行操作,此时可以调用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一行行代码对比后才发现需要添加一个权限才能实现启动
<uap:Extension Category="windows.personalAssistantLaunch"/>
<uap:Extension Category="windows.personalAssistantLaunch"/>
<uap:Extension Category="windows.personalAssistantLaunch"/>
重要的事情要说三遍、三遍、三遍。
添加后应用完美启动。
备注:
VoiceCommandContentTile的Title要有区分性,调用RequestDisambiguationAsync的时候会报错。
2、前台启动
待续...


浙公网安备 33010602011771号