Spracherkennung auf Raspberry PI 2 mit Windows 10 IoT
Windows 10 IoT Core auf dem Raspberry PI 2
Mit Windows 10 ist nun auch eine abgespeckte Version verfügbar, welche speziell auf IoT-Bedürfnisse (IoT = Internet of Things) zugeschnitten ist. Höchste Zeit also für uns, das Ganze mal genauer unter die Lupe zu nehmen.
Mit dem Raspberry PI 2 steht eine leistungsfähige Hardware-Plattform zur Verfügung mit wir einfach Prototypen erstellen können. Zu unserer Freude ist das nun mit Visual Studio 2015 in C#/.NET 4.6 möglich. Man kann entweder eine sogenannte “headless” Applikation erstellen, welche ohne User-Interface im Hintergrund läuft oder man erstellt eine Universal-App, welche bei angeschlossenem Bildschirm auch ein User-Interface darstellt. Eine Universal-App ist übrigens auch die einfachste Möglichkeit, wenn man Sprachausgabe realisieren will (nicht Teil dieses Artikels). Headless-Apps tun sich hier noch etwas schwer.
Wie man Windows 10 IoT auf das embedded Device, in unserem Falle den Raspberry PI 2, bekommt und anschliessend mit dem Visual Studio verbindet, findet man ausführlich auf dem ms-iot Github.
Demoprojekt
Nun zu unserem Demo-Projekt:
Mit Windows 10, das ja auf Phones, Tablets, Notebooks, Desktops und embedded Devices läuft, ist das Thema Sprachsteuerung wieder populärer geworden. Probieren wir also aus, ob sich mit Windows 10 IoT Core Devices mittels Sprachkommandos steuern lassen.
Leider ist das momentan nur in englischer Sprache möglich, da man das Windows 10 IoT Core Image noch nicht mit deutschen Sprachpaketen bekommt und leider auch nicht selbst nachrüsten kann.
Als Aufgabe stellen wir uns, dass wir zwei LEDs an den Ausgängen mittels Sprachkommandos ein- und ausschalten und unterschiedlich schnell blinken lassen können.
Wie das aussieht, zeigt das folgende Video:
Wir schalten also eine grüne LED an GPIO Pin 4 und eine rote LED an GPIO Pin 5.
Achtung: Die GPIO Nummerierung entspricht nicht der Stecker-Pin-Nummerierung.
Zu diesem Zweck erstellen wir eine Windows Universal App, welche eine Klasse SpeechController für die Spracherkennung, eine Klasse IoController für das Hardware Interface und eine Klasse Blinker für die Blink-Logik enthält. Diese Teile werden durch eine Logik-Klasse miteinander verheiratet.
SpeechController
Der SpeechController wird so initialisiert und konfiguriert, dass kontinuierliche Spracherkennung läuft. Es werden dann Events gefeuert, wenn Sprache erkannt wurde.
recognizer = new SpeechRecognizer(); recognizer.ContinuousRecognitionSession.ResultGenerated += RecognizerResultGenerated; recognizer.ContinuousRecognitionSession.AutoStopSilenceTimeout = TimeSpan.MaxValue;
Damit der SpeechRecognizer versteht, was man ihm sagt, muss ihm die Grammatik mitgeteilt werden. Die Grammatik schränkt somit ein, was ein SpeechRecognizer versteht.
Es gibt unterschiedliche Arten von Grammatik:
- dictation grammar: Für freies Diktieren von Text, z.B. eines E-Mails.
- web search grammar: Für Suche im Web (über Bing).
- list grammar: Liste (Array) von einfachen Befehlen
- SRGS grammar: Komplexere Grammatik definiert im XML-Format nach dem SRGS-Standard (Speech Recognition Grammar Specification)
Leider können die unterschiedlichen Grammatiken nur bedingt kombiniert werden. Dication Grammar und Web Search Grammar können nur einzeln benutzt werden und lassen sich nicht kombinieren. List grammar und SRGS grammar lassen sich hingegen gemeinsam nutzen, können aber auch nicht mit den ersten beiden kombiniert werden.
Für unseren Anwendungsfall eignet sich das SRGS grammar. Wir definieren also unsere Grammatik im XML-File, so dass folgende Kommandos möglich sind:
- switch (ON|OFF) pin (GPIO_NUMBER)
- z.B. switch ON pin 5 um LED an GPIO 5 einzuschalten
- z.B. switch OFF pin 4 um LED an GPIO 4 auszuschalten
- blink pin (GPIO_NUMBER) [FASTER|SLOWER]
- z.B. blink pin 5 um einfach LED an GPIO 5 blinken zu lassen
- z.B. blink pin 5 faster um einfach LED an GPIO 5 schneller blinken zu lassen
- z.B. blink pin 5 slower um einfach LED an GPIO 5 langsamer blinken zu lassen
Die gesamte SRGS-Grammatik sieht wie folgt aus:
<?xml version="1.0" encoding="utf-8" ?> <grammar version="1.0" xml:lang="en-US" root="root" xmlns="http://www.w3.org/2001/06/grammar" tag-format="semantics/1.0"> <rule id="root" scope="public"> <one-of> <item> <ruleref uri="#switchCommand"/> <tag> out.command="SWITCH"; </tag> <tag> out.params=rules.switchCommand; </tag> </item> <item> <ruleref uri="#blinkCommand"/> <tag> out.command="BLINK"; </tag> <tag> out.params=rules.blinkCommand; </tag> </item> </one-of> </rule> <rule id="switchCommand" scope="public"> <item> <item> switch </item> <item> <ruleref uri="#ioStates" /> <tag> out.state=rules.latest(); </tag> </item> <item> pin </item> <item> <ruleref uri="#ioNumber" /> <tag> out.pin=rules.latest(); </tag> </item> </item> </rule> <rule id="ioStates"> <one-of> <item> on <tag> out="ON"; </tag> </item> <item> off <tag> out="OFF"; </tag> </item> </one-of> </rule> <rule id="blinkCommand" scope="public"> <item> <item> blink </item> <item> pin </item> <item> <ruleref uri="#ioNumber" /> <tag> out.pin=rules.latest(); </tag> </item> <item repeat="0-1"> <ruleref uri="#blinkSpeeds" /> <tag> out.speed=rules.latest(); </tag> </item> </item> </rule> <rule id="blinkSpeeds"> <one-of> <item> faster <tag> out="FASTER"; </tag> </item> <item> slower <tag> out="SLOWER"; </tag> </item> </one-of> </rule> <rule id="ioNumber"> <one-of> <item> two <tag> out = 2; </tag> </item> <item> three <tag> out = 3; </tag> </item> <item> four <tag> out = 4; </tag> </item> <item> five <tag> out = 5; </tag> </item> <item> six <tag> out = 6; </tag> </item> <item> seven <tag> out = 7; </tag> </item> <item> eight <tag> out = 8; </tag> </item> <item> nine <tag> out = 9; </tag> </item> <item> ten <tag> out = 10; </tag> </item> <item> eleven <tag> out = 11; </tag> </item> <item> twelve <tag> out = 12; </tag> </item> <item> thirteen <tag> out = 13; </tag> </item> <item> fourteen <tag> out = 14; </tag> </item> <item> fifteen <tag> out = 15; </tag> </item> <item> sixteen <tag> out = 16; </tag> </item> <item> seventeen <tag> out = 17; </tag> </item> <item> eighteen <tag> out = 18; </tag> </item> <item> nineteen <tag> out = 19; </tag> </item> <item> twenty <tag> out = 20; </tag> </item> <item> twentyone <tag> out = 21; </tag> </item> </one-of> </rule> </grammar>
Im root-Tag werden die Kommandos für switch und blink definiert. Hierarchisch geht es nun über die ruleref-Elemente zu den rules mit den entsprechenden IDs.
Die tag-Elemente definieren die Tags, welche nach der Erkennung dem Entwickler zur Verfügung stehen, um herauszufinden, welche Regeln bei der letzten Eingabe angewendet wurden.
<tag> out.command=”SWITCH”; </tag> liefert zum Beispiel ein Tag mit dem Key “command” und dem value “SWITCH”.
Damit die Regeln angewandt werden, muss nun zuerst die Grammatik aus dem XML-File eingelesen und als Constraint zum SpeechRecognizer hinzugefügt werden.
Anschliessend müssen die Constraints im Recognizer kompiliert werden. Hat man sich im SGRS-File vertippt, gibt das einen Fehler, ansonsten ist der Status “Success” und damit kann die kontinuierliche Spracherkennung gestartet werden.
var grammarContentFile = await Package.Current.InstalledLocation.GetFileAsync(GrammarFile); var grammarConstraint = new SpeechRecognitionGrammarFileConstraint(grammarContentFile); recognizer.Constraints.Add(grammarConstraint); var compilationResult = await recognizer.CompileConstraintsAsync(); if (compilationResult.Status == SpeechRecognitionResultStatus.Success) { await recognizer.ContinuousRecognitionSession.StartAsync(); }
Nun hört der Raspberry PI laufend zu und versucht das Gesagte auf die definierten Regeln abzubilden.
Wenn ein Sprachfetzen erkannt wurde, wir nun das Event ResultGenerated des SpeechRecognizer aufgerufen. In args.Result.SemanticInterpretation.Properties sind nun die Tags der angewendeten Regeln als Key-Value-Pairs zu finden, wobei das Value jeweils eine Liste von Strings ist, da mehrere Werte möglich sind.
// Action Handler, called when sucessfull speech results are available public Action<IReadOnlyDictionary<string, IReadOnlyList<string>>> ProcessCommand { get; set; } private void RecognizerResultGenerated(SpeechContinuousRecognitionSession session, SpeechContinuousRecognitionResultGeneratedEventArgs args) { Debug.WriteLine("status: " + args.Result.Status); Debug.WriteLine("text: " + args.Result.Text); Debug.WriteLine("confidence: " + args.Result.Confidence); switch (args.Result.Confidence) { case SpeechRecognitionConfidence.Low: Debug.WriteLine("Sorry, I did not understand. Could you please repeat?"); break; case SpeechRecognitionConfidence.Rejected: Debug.WriteLine("Sorry, this is not something I could do for you."); break; case SpeechRecognitionConfidence.Medium: case SpeechRecognitionConfidence.High: Debug.WriteLine("You said: " + args.Result.Text); var count = args.Result.SemanticInterpretation.Properties.Count; Debug.WriteLine("Count: " + count); Debug.WriteLine("Tag: " + args.Result.Constraint.Tag); foreach (var prop in args.Result.SemanticInterpretation.Properties) { Debug.WriteLine("Property " + prop.Key + ":"); foreach (var value in prop.Value) { Debug.WriteLine(" value = " + value); } } ProcessCommand(args.Result.SemanticInterpretation.Properties); break; } }
Im registrierten ResultGenerated-Eventhandler wird zuerst geprüft, ob überhaupt zuverlässige Resultate vorliegen. Die Confidence-Eigenschaft des Results liefert hier die Werte Low (schlechte Erkennungsqualität), Rejected (Spracheingabe wurde abgelehnt, passt nicht zur Grammatik), Medium (es wurde eine Spracheingabe passend zu Grammatik erkannt, aber nicht alles passt 100%-ig), High (zweifelsfrei auf Grammatik abgebildet).
Mit SpeechRecognitionConfidence gleich Medium oder High kann eine weitere Verarbeitung erfolgen, da die Tags dann abgefüllt wurden.
Um dem SpeechController generell zu halten, haben wir das in die Action ProcessCommand ausgelagert, welche dann anderswo implementiert und mit anderen Applikationsteilen wie dem IoController oder Blinker verbunden werden kann, wie das folgende Beispiel zeigt:
public sealed class Logic { private readonly SpeechController speechController; private readonly IoController ioController; private readonly Blinker redBlinker; private readonly Blinker greenBlinker; public Logic() { speechController = new SpeechController(); speechController.ProcessCommand = ProcessCommand; ioController = new IoController(); ioController.ConfigureOutput("green", 4); ioController.ConfigureOutput("red", 5); greenBlinker = new Blinker(ioController, 4, 1000); redBlinker = new Blinker(ioController, 5, 1000); } private void ProcessCommand(IReadOnlyDictionary<string, IReadOnlyList<string>> tags) { if (tags.ContainsKey("command")) { if (tags["command"].Contains("SWITCH")) { if (tags.ContainsKey("pin") && tags.ContainsKey("state")) { int pin = int.Parse(tags["pin"][0]); if (pin == 4) greenBlinker.Stop(); if (pin == 5) redBlinker.Stop(); if (tags["state"][0] == "ON") { ioController.TurnOn(pin); } if (tags["state"][0] == "OFF") { ioController.TurnOff(pin); } } } else if(tags["command"].Contains("BLINK")) { if (tags.ContainsKey("pin")) { int pin = int.Parse(tags["pin"][0]); if (pin == 4) greenBlinker.Start(); if (pin == 5) redBlinker.Start(); if (tags.ContainsKey("speed")) { if (tags["speed"][0] == "FASTER") { if (pin == 4) greenBlinker.BlinkFaster(); if (pin == 5) redBlinker.BlinkFaster(); } if (tags["speed"][0] == "SLOWER") { if (pin == 4) greenBlinker.BlinkSlower(); if (pin == 5) redBlinker.BlinkSlower(); } } } } } } }
Dieser Code verbindet die Spracherkennung im SpeechController mit den Funktionen für die Ansteuerung der LED’s, indem einfach auf das Vorhandensein von Tags und gegebenenfalls auf deren Werte geprüft wird. Da ein Tag grundsätzlich mehrere Werte haben könnte, wir aber immer nur einen Wert zurückliefern, wird hier direkt auf den Index 0 zugegriffen.
Der Vollständigkeit halber sollen hier noch die beiden Klassen für IoController und Blinker aufgeführt werden. Für das Verständnis der Spracherkennung sind sie nicht wichtig.
IoController ist eine Abstraktion der Raspberry PI GPIOs und Blinker bietet Blink Funktionalität für einen spezifischen GPIO Pin aufbauend auf dem IoController.
IoController:
using System.Collections.Generic; using System.Linq; using Windows.Devices.Gpio; namespace SpeechConfigApp { public sealed class IoController { private readonly IDictionary<string, GpioPin> ios = new Dictionary<string, GpioPin>(); private static GpioController gpio; public IoController() { gpio = GpioController.GetDefault(); } public void ConfigureInput(string name, int pin) { Configure(name, pin, GpioPinDriveMode.Input); } public void ConfigureOutput(string name, int pin) { Configure(name, pin, GpioPinDriveMode.Output); } public void TurnOn(string name) { WriteGpioPin(name, GpioPinValue.Low); } public void TurnOn(int pin) { GpioPin gpioPin = ios.Values.Single(io => io.PinNumber == pin); gpioPin.Write(GpioPinValue.High); } public void TurnOff(string name) { WriteGpioPin(name, GpioPinValue.Low); } public void TurnOff(int pin) { GpioPin gpioPin = ios.Values.Single(io => io.PinNumber == pin); gpioPin.Write(GpioPinValue.Low); } public bool? GetValue(string name) { GpioPin pin; if (ios.TryGetValue(name, out pin)) { var value = pin.Read(); return value == GpioPinValue.High; } return null; } private void Configure(string name, int pin, GpioPinDriveMode mode) { GpioPin gpioPin = gpio.OpenPin(pin); gpioPin.SetDriveMode(mode); ios.Add(name, gpioPin); } private void WriteGpioPin(string name, GpioPinValue value) { GpioPin pin; if (ios.TryGetValue(name, out pin)) { pin.Write(value); } } } }
Blinker:
using System; using System.Diagnostics; using Windows.System.Threading; namespace SpeechConfigApp { public sealed class Blinker { private readonly int pinNr; private readonly IoController ioController; private bool pinValue; private ThreadPoolTimer timer; public Blinker(IoController ioController, int pinNr, double intervall = 500) { this.pinNr = pinNr; this.ioController = ioController; Intervall = intervall; } public double Intervall { get; private set; } public void BlinkFaster() { Intervall = Intervall/2; Stop(); if(Intervall >= 1) { Start(); } } public void BlinkSlower() { Intervall = Intervall * 2; Stop(); Start(); } public void Start() { if (timer == null) { ioController.TurnOn(pinNr); timer = ThreadPoolTimer.CreatePeriodicTimer(TimerTick, TimeSpan.FromMilliseconds(Intervall)); Debug.WriteLine("Start blinking on Pin {0}", pinNr); } } public void Stop() { if (timer != null) { timer.Cancel(); timer = null; ioController.TurnOff(pinNr); Debug.WriteLine("Stop blinking on Pin {0}", pinNr); } } private void TimerTick(ThreadPoolTimer timer) { TogglePin(); } private void TogglePin() { if (pinValue) { ioController.TurnOff(pinNr); } else { ioController.TurnOn(pinNr); } pinValue = !pinValue; } } }
Zusammenfassung
Die Programmierung von sprachgesteuerten Geräten ist mit Windows 10 IoT Core ist nicht komplizierter als von sprachgesteuerten Apps auf Windows Phone. Dies ist vor allem der neuen Strukturierung des .NET Frameworks und den Universal Apps zu verdanken.
Die Entwicklung mit Visual Studio 2015 erwies sich als sehr angenehm, verglichen mit anderen Embedded IDEs. Anders als bei Windows Phone ist bei IoT leider keine Entwicklung mittels Emulator möglich. Man muss immer direkt mit dem embedded Device (hier Raspberry PI 2) über Ethernet-Kabel verbunden sein.
Leider ist es zum heutigen Zeitpunkt auch noch nicht möglich eine eigenes Image für Raspberry PI 2 mit Windows 10 IoT erstellen, so dass es uns nicht möglich war ein lokalisiertes (deutsches) Image zu erstellen. Hoffentlich ändert sich das bald.