Sprachbefehle (Cortana) in Windows Phone Apps integrieren

Cortana ist die Sprachassistentin auf Windows Phone 8.1. Sie ist darauf ausgelegt, Befehle in natürlich gesprochener Sprache zu empfangen und auf dem System zu verarbeiten. Für App-Entwickler eröffnen sich dadurch ganz neue Möglichkeiten, eigene Apps zu starten und mit Sprachbefehlen zu steuern.
Der folgende Artikel zeigt die Schritte auf, die notwendig sind, um Sprachbefehle (Cortana) in eigene Windows Phone Apps zu integrieren.

Schnellstart: Sprachbefehle (Cortana) in Windows Phone Apps integrieren

Um Cortana in eigenen Apps zu nutzen, sind folgende Schritte notwendig:

  • App für Sprachbefehle vorbereiten
  • Voice Command Definition (VCD) erstellen
  • Die in der VCD definierten Sprachbefehle in der App registrieren
  • Sprachbefehle in der App verarbeiten

Im Folgenden werden die einzelnen Schritte anhand einer Beispiel-App näher erläutert. Diese ist bewusst sehr einfach gehalten und soll primär die gesprochenen Worte entgegennehmen und anzeigen. Den Sourcecode der App (CortanaIntegrationDemo) als Visual Studio Solution gibt es weiter unten zum Download.

App für Sprachbefehle vorbereiten

Ausgangssituation ist zunächst einmal ein neues Visual Studio Projekt für Windows Phone 8.1 (Blank App).

Damit die App überhaupt Sprachbefehle entgegen nehmen kann, müssen zunächst Änderungen im App-Manifest vorgenommen werden: Hier muss unter Capabilities die Option für den Zugriff auf das Mikrofon gesetzt werden. Ohne diese Einstellung treten Exceptions beim Zugriff auf die Speech-Recognition-APIs auf.

Setzen der App-Berechtigungen für Sprachbefehle
Setzen der App-Berechtigungen für Sprachbefehle

Die Berechtigung für den Zugriff auf das Internet wird ebenso benötigt, da die Sprachbefehle nicht lokal auf dem Smartphone, sondern zunächst online auf Microsoft-Servern verarbeitet werden. Allerdings ist diese Berechtigung standardmäßig bei einer neu erstellen Windows Phone Solution bereits aktiviert.

Voice Command Definition (VCD) erstellen

Eine Voice Command Definition ist im Grunde genommen nur eine XML-Datei, in der festgelegt wird, welche Sprachbefehle von der App entgegen genommen werden können. Außerdem kann in einer VCD-Datei definiert werden, auf welche Seite (Page) beim jeweiligen Sprachbefehl navigiert werden soll.

Dazu wird dem Projekt einfach eine neue Datei des Typs Voice Command Definition hinzugefügt:

Voice Command Definition hinzufügen
Voice Command Definition hinzufügen

Daraufhin werden die einzelnen Sprachbefehle in dieser Datei definiert. Dafür können unterschiedliche XML-Elemente verwendet werden (siehe MSDN-Link):

  • CommandSet: Stellt ein eine komplette Beschreibung der Befehle für eine Sprache dar. Wenn die App mehrere Sprachen unterstützt, sollten auch mehrere CommandSets definiert werden, z.B. en-US für Englisch und de-DE für Deutsch.
  • CommandPrefix: Hierdurch wird das „Schlüsselwort“ definiert, durch das Windows weiß, dass der folgende Sprachbefehl an die entsprechende App weitergegeben werden muss. Im Normalfall ist dies einfach der Name der App. Pro Sprache (also pro CommandSet) kann ein CommandPrefix definiert werden, allerdings sollte sich die App unter verschiedenen Sprachen gleich verhalten, daher ist es empfehlenswert, hier immer das gleiche CommandPrefix zu verwenden.
  • Command: Definiert genau einen Befehl, den die App verarbeiten kann. Wenn mehrere Befehle verfügbar sein sollen, müssen auch mehrere Commands angelegt werden. Ein Command beinhaltet dabei weitere wichtige Elemente:
    • ListenFor: Definition, auf was die App hören soll. Beschreibt den genauen Aufbau eines Befehls.
    • Feedback: Wenn ein Sprachbefehl erkannt wurde und Cortana diesen Befehl an die App weitergibt, wird ein kurzes Feedback zusammen mit einem Fortschrittsbalken angezeigt.
    • NavigateTo: Gibt die (XAML-)Page an, zu der bei der Erkennung dieses Commands navigiert werden soll.
  • PhraseList: Über eine PhraseList werden bestimmten (feststehende) Begriffe definiert, die in den Sprachbefehlen zum Einsatz kommen können.
  • PhraseTopics: Ähnlich wie bei der PhraseList können hier Begrifflichkeiten definiert werden, die in den Commands zum Einsatz kommen können. Im Unterschied zu einer PhraseList werden bei einem PhraseTopic jedoch keine feststehenden Begriffe definiert, sondern Platzhalter für beliebige gesprochene Wörter.
  • Example: Gibt ein kurzes Beispiel des Befehls. Kann unter mehreren Elementen verwendet werden. Diese Beispiele werden angezeigt, wenn der Benutzer die Hilfe zur App über Cortana aufruft (Cortana > „Hilfe“).

In der VCD-Datei des Beispiel-Projekts sind zwei CommandSets (Deutsch und Englisch) definiert. Das Schlüsselwort für die App lautet „Papagei“ (bzw. Englisch „parrot“), da die App im Grunde genommen nur den gesprochenen Text wiedergibt. Die komplette VCD-Datei sieht daher folgendermaßen aus.

<?xml version="1.0" encoding="utf-8"?>

<VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.1">
  <CommandSet Name="commandSetDeDe" xml:lang="de-DE">
    <CommandPrefix>Papagei</CommandPrefix>
    <Example> Papagei sag 'Hallo Welt' </Example>

    <Command Name="Say">
      <Example> sag 'Hallo Welt' </Example>
      <ListenFor> sag {dictatedText} </ListenFor>
      <Feedback> Papagei sagt... </Feedback>
      <Navigate Target ="SayPage.xaml"/>
    </Command>

    <Command Name="Eat">
      <Example> esse Karotte </Example>
      <ListenFor> esse {food} </ListenFor>
      <Feedback> Papagei isst... </Feedback>
      <Navigate Target="EatPage.xaml"/>
    </Command>

    <Command Name="Help">
      <Example> hilfe </Example>
      <ListenFor> hilfe </ListenFor>
      <Feedback> Papagei hilft... </Feedback>
      <Navigate />
    </Command>

    <PhraseList Label="food">
      <Item> Karotte </Item>
      <Item> Apfel </Item>
      <Item> Nuss </Item>
    </PhraseList>

    <PhraseTopic Label="dictatedText" Scenario="Dictation">
      <Subject>Dictated text</Subject>
    </PhraseTopic>

  </CommandSet>

  <CommandSet Name="commandSetEnUs" xml:lang="en-US">
    <CommandPrefix>Parrot</CommandPrefix>
    <Example> Parrot say 'Hello world' </Example>

    <Command Name="Say">
      <Example> say 'Hello world' </Example>
      <ListenFor> say {dictatedText} </ListenFor>
      <Feedback> Parrot says... </Feedback>
      <Navigate Target ="SayPage.xaml"/>
    </Command>

    <Command Name="Eat">
      <Example> eat carrot </Example>
      <ListenFor> eat {food} </ListenFor>
      <Feedback> Parrot eats... </Feedback>
      <Navigate Target="EatPage.xaml"/>
    </Command>

    <Command Name="Help">
      <Example> help </Example>
      <ListenFor> help </ListenFor>
      <Feedback> Parrot helps... </Feedback>
      <Navigate />
    </Command>

    <PhraseList Label="food">
      <Item> carrot </Item>
      <Item> apple </Item>
      <Item> nut </Item>
    </PhraseList>

    <PhraseTopic Label="dictatedText" Scenario="Dictation">
      <Subject>Dictated text</Subject>
    </PhraseTopic>

  </CommandSet>
</VoiceCommands>

 VCD in der App registrieren

Im nächsten Schritt muss die eben erzeugte VCD in der App (oder besser gesagt dem System) registriert werden.

Dies wird am besten in der Klasse NavigationHelper durchgeführt. Diese Klasse wird beim Hinzufügen einer weiteren Seite zur Solution von Visual Studio automatisch angelegt. Daher: Falls die NavigationHelper noch nicht vorhanden sind, einfach eine neue Page (BasicPage) zur Solution hinzufügen und folgende Meldung mit Ja bestätigen.

Hinzufügen von fehlenden Dateien
Hinzufügen von fehlenden Dateien

Die neu angelegte Page kann danach wieder aus der Solution entfernt werden.

Anschließend befindet sich die Klasse NavigationHelper im Ordner Common in der Solution. Hier sucht man nun die Methode OnNavigatedTo. Direkt nach der ersten if-Abfrage wird dann die Registrierung der VoiceCommands durchgeführt:

if (e.NavigationMode == NavigationMode.New)
{
	try
	{
		// Register VCD on the system
		var storageFile = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appx:///VoiceCommandDefinition.xml"));
		await Windows.Media.SpeechRecognition.VoiceCommandManager.InstallCommandSetsFromStorageFileAsync(storageFile);
	}
	catch (Exception)
	{
		// You will get a UnauthorizedAccessException here if you forgot to set the app's capabilities ('Microphone').
	}
	
	// ...

Dies sorgt dafür, dass bei ersten Starten der App die Registrierung automatisch vorgenommen wird.

Meine Erfahrung zeigt, dass es „da draußen“, d.h. wenn die App erst einmal im App Store verfügbar ist, hin und wieder zu nicht reproduzierbaren Exceptions bei der Registrierung der VCD-Datei kommen kann. Um einen App-Absturz zu vermeiden, fange ich hier alle Exceptions ab. Alle Typen von Exceptions abzufangen (und einfach zu „verschlucken“) ist zwar kein empfohlener Programmierstil, aber an dieser Stelle immer noch besser, als das Abstürzen der App zu riskieren.

Hinweis: Damit die NavigationHelper korrekt eingebunden werden, empfiehlt es sich, dass die MainPage des Projektes als BasicPage angelegt wird. Dazu einfach die MainPage aus dem Projekt löschen und neu anlegen (diesmal als BasicPage). Dies sorgt dafür, dass diese Page bereits gewisse Basisfunktionalitäten mitbringt, die nicht manuell implementiert werden müssen.

Sprachbefehle in der App verarbeiten

Abschließend muss noch definiert werden, wie die App auf die einzelnen Sprachbefehle reagieren soll, damit diese korrekt verarbeitet werden.

Zunächst muss der Sprachbefehl analysiert werden, was am besten in der App.xaml.cs erledigt wird. Die passende Methode ist hier OnActivated:

protected override void OnActivated(IActivatedEventArgs args)
{
	base.OnActivated(args);

	// Check if a voice command activated the app.
	if (args.Kind == Windows.ApplicationModel.Activation.ActivationKind.VoiceCommand)
	{
		var commandArgs = args as Windows.ApplicationModel.Activation.VoiceCommandActivatedEventArgs;
		Windows.Media.SpeechRecognition.SpeechRecognitionResult speechRecognitionResult = commandArgs.Result;

		var voiceCommandName = speechRecognitionResult.RulePath[0]; // The name of the voice command used.
		var textSpoken = speechRecognitionResult.Text; // The actual text spoken.
		var navigationTarget = speechRecognitionResult.SemanticInterpretation.Properties["NavigationTarget"][0]; // The navigation target of the command (from NavigateTo element in VCD file).

		Frame rootFrame = Window.Current.Content as Frame;

		if (rootFrame == null)
			throw new Exception("Root frame missing");

		switch (voiceCommandName)
		{
			case "Say":
				// 'Say' command: Find out what was spoken by checking the PhraseTopic defined in VCD file.
				// speechRecognitionResult.Text would not be sufficient here, because this will give you the whole text what was spoken alfter the CommandPrefix (e.g. "say hello world").
				// This is the reaseon why we need a PhraseTopic defined in the VCD file and cannot simply use the {*} operator in the 'Say' command.
				IReadOnlyList<string> sayList = null;

				// The first parameter is the name of the PhraseTopic defined in the VCD file.
				if (speechRecognitionResult.SemanticInterpretation.Properties.TryGetValue("dictatedText", out sayList))
				{
					var say = sayList[0]; // Just take first one.
					rootFrame.Navigate(typeof(SayPage), say); // And navigate to target page.
				}
				
				break;
			case "Eat":
				// 'Eat' command: check if some food from the PhraseList 'food' was recognized.
				IReadOnlyList<string> foodList = null;

				if (speechRecognitionResult.SemanticInterpretation.Properties.TryGetValue("food", out foodList))
				{
					var foodToEat = foodList[0]; // Just take first one.
					rootFrame.Navigate(typeof(EatPage), foodToEat); // And navigate to target page.
				}
				break;
			case "Help":
				// 'Help' command: This is just the normal start of the app.
				rootFrame.Navigate(typeof(MainPage)); // Simply navigate to the MainPage.
				break;
			default:
				break;
		}

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

Nachdem die einzelnen Bestandteile des Sprachbefehls identifiziert wurden, wird die Navigation zur jeweiligen Page angestoßen, die den Befehl verarbeitet. Wichtig ist in diesem Fall der Parameter, der an die Page übergeben wird.
Dieser kann in der Code-behind Datei der Page in der überschriebenen Methode OnNavigatedTo empfangen und weiter verarbeitet werden. In unserem Fall (SayPage) enthält der Parameter den gesprochenen Text, der einfach nur angezeigt wird:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
	this.SayTextBlock.Text = e.Parameter as string;
}

Bei komplexeren Szenarien empfiehlt es sich, das „Zerpflücken“ des Sprachbefehls und die Verarbeitung in eine separate Klasse auszulagern.

Besonderheiten und weitere Schritte

Im Grunde genommen sind lediglich die oben genannten Schritte für eine Integration von Cortana in die eigenen Apps notwendig.
Allerdings gibt es einige Besonderheiten bzw. weiterführende Schritte, die man noch beachten sollte:

Lokalisierung

Wenn eine App mehrere Sprachen unterstützen soll, muss die App entsprechend lokalisiert werden. Die grundsätzliche Vorgehensweise bei der Lokalisierung von Apps würde jedoch den Rahmen des Artikels sprengen und soll daher nicht weiter behandelt werden. Allerdings unterstützt die Beispiel-App zu Demonstrationszwecken zwei Sprachen (Deutsch und Englisch).

Im Rahmen der Lokalisierung ist es bzgl. der Cortana-Integration auch wichtig, dass die Sprachbefehle an weitere Sprachen angepasst werden. Hier sind Erweiterungen in der VCD-Datei notwendig (Hinzufügen weiterer CommandSets für jede unterstütze Sprache).

Dynamisches Laden der PhraseTopics

Manchmal sind die PhraseTopics, die in der VCD-Datei festgelegt werden, nicht statisch. Dies ist insbesondere dann der Fall, wenn die PhraseTopics von Benutzer-Eingaben abhängen. In diesem Fall können PhraseTopics auch dynamisch geladen werden:

public async static void SetPhraseLists(IEnumerable<string> data)
{
	VoiceCommandSet voiceCommandSet;

	if (Windows.Media.SpeechRecognition.VoiceCommandManager.InstalledCommandSets.TryGetValue("commandSetEnUs", out voiceCommandSet))
	{
		await voiceCommandSet.SetPhraseListAsync("food", data);
	}

	if (Windows.Media.SpeechRecognition.VoiceCommandManager.InstalledCommandSets.TryGetValue("commandSetDeDe", out voiceCommandSet))
	{
		await voiceCommandSet.SetPhraseListAsync("food", data);
	}
}

Diese Methode muss dabei immer dann ausgeführt werden, wenn sich an den Begriffen (durch den Benutzer) etwas geändert hat.
Beim dynamischen Laden eines PhraseTopics wird bei Bezug auf die Definition in der VCD-Datei genommen, wie folgende Grafik zeigt:

Dynamisches Laden eines PhraseTopics
Dynamisches Laden eines PhraseTopics

Die Beispiel-App

Es steht eine Beispiel-App zur Verfügung, in der die in diesem Artikel gezeigten Inhalte implementiert sind. Diese App wurde mit Visual Studio 2013 für Windows Phone 8.1 entwickelt.

Die Beispiel-App bietet zwar nicht sonderlich viele Funktionen, kann allerdings gut genutzt werden, um die ersten Gehversuche mit der Cortana-Integration zu unternehmen. Ich empfehle, einfach ein wenig mit der App herumzuspielen, um ein Gefühl für die Speech-Recognition API zu bekommen.

Download CortanaIntegrationDemo.zip

Und was ist mit Windows-Apps?

Cortana ist momentan nur auf Windows Phone 8.1 verfügbar. Daher kann man die hier gezeigte Vorgehensweise nicht bei Apps anwenden, die für Windows entwickelt werden.

Dies wird sich allerdings mit Windows 10 ändern, da Cortana auch integraler Bestandteil des Betriebssystems werden wird. Bis dahin muss man v.a. in Universal Apps immer eine Unterscheidung zwischen den Plattformen vornehmen und die Cortana-Integration nur in dem Teil implementieren, der für Windows Phone zuständig ist.

Links

Kommentar verfassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert