Eigenbau-Sprachassistent Teil 2: Verknüpfte Komponenten und erste Funktion
Im letzten Teil dieser Serie haben wir uns mit der Software-Auswahl und -Installation für einen selbstgebauten Sprachassistenten ohne Cloud beschäftigt. Dieser Teil dreht sich um die Verzahnung von Rhasspy und Node-RED. Am Ende wird der Assistent seine erste Funktion erlernen.
Soll-Zustand
Wie wir im letzten Teil bereits gelernt haben, ist Rhasspy nicht in der Lage, Intents selbstständig auszuführen - es benötigt dafür ein externes Tool, wie beispielsweise Node-RED.
Rhasspy soll erkannte Satzfragmente (Sentences) den dazugehörigen Kommandos (Intents) zuordnen können und diese Kommando-Anfragen an Node-RED weiterleiten. Node-RED wiederum führt - je nach eingegangenem Kommando - eine Funktion aus und sendet die Antwort der Funktion an Rhasspy zurück. Zuletzt wird die erhaltene Antwort über die Lautsprecher vorgelesen.
Als Beispiel hierfür soll das Vorlesen des aktuellen Datums und der Uhrzeit dienen.
Den gesamten Code dieses und der folgenden Artikel gibt es auch auf GitHub
Vorbereitungen
Bevor der Sprachassistent genutzt wird, empfiehlt es sich, das Rufwort sowie die Sprachausgabe zu konfigurieren.
Für das Rufwort habe ich mich für Porcupine entschieden, da es bedeutend zuverlässiger als das standardmäßig verwendete Pocketsphinx arbeitet. Das standardmäßige Rufwort gleicht dem Projektnamen - auf GitHub gibt es jedoch für den Raspberry Pi vorkompilierte Muster zum Download (mein persönlicher Favorit ist "Terminator"). Eigene Rufwörter erfordern derzeit eine kommerzielle Lizenz.
Zum Aktivieren genügend Klicks auf Settings > Wake Word > Use Porcupine on this device in der Rhasspy-Oberfläche. Nach erfolgtem Speichern der Einstellungen werden noch eine Bibliothek sowie das Erkennungsmuster heruntergeladen:
Nach erfolgtem Download befindet sich das Sprachmuster im Rhasspy-Profilordner unter rhasspy/profiles/de/porcupine
. In diesen Ordner lassen sich auch die oben erwähnten alternativen Rufworte ablegen - die Konfiguration muss entsprechend angepasst werden - beispielsweise:
Keyword Files: porcupine/terminator_porcupine.ppn
Für die Sprachausgabe wird nach der Rhasspy-Installation eSpeak verwendet - welches sehr blechern und unnatürlich klingt. In einem anderen Blog-Artikel habe ich beschrieben, wie sich das deutlich angenehmere Pico TTS auf Raspbian übersetzen und installieren lässt.
Mich persönlich stören die Flöten-ähnlichen Geräusche während der Spracherkennung - glücklicherweise finden sich im vorgefertigten Docker-Container alternative, deutlich angenehmere Geräusche. Diese können mit Klicks auf Settings > Sounds konfiguriert werden:
Hier nochmal der Pfad zum Kopieren: /usr/share/rhasspy/.venv/lib/python3.7/site-packages/snowboy/resources
Abschließend sorgt ein Klick auf Train dafür, dass die vorgenommenen Änderungen in die Logik des Assistenten aufgenommen werden.
Verknüpfen von Rhasspy und Node-RED
Die eigentliche Verknüpfung der beiden Systeme ist recht trivial. In der Rhasspy-Oberfläche genügen Klicks auf Settings und Intent Handling sowie die Angabe der Handler-URL. Diesen müssen wir noch in Node-RED erstellen, können aber schon mal die URL hinterlegen:
http://
Intent-Aufbau und -Handler
Nun ist es an der Zeit sich den Aufbau eines Intents anzuschauen - hierzu erstellen wir uns einen Flow in Node-RED, der auf HTTP-Anfragen reagiert. Intents werden von Rhasspy über POST versendet.
Zuerst ziehen wir uns ein http in-Element aus der Werkzeugleiste links in das Raster und konfigurieren es wie folgt:
Methode | POST |
---|---|
Datei-Uploads aktivieren? | Nein |
URL | /intent |
Name | Detect intent |
Anschließend ziehen wir ein debug-Element direkt neben das http in-Element und verknüpfen die beiden Nodes mit einer Verbindung. So wird der Inhalt der eingegangenen Anfrage direkt ausgegeben und wir können uns den entsprechenden Aufbau anschauen.
Der Aufbau sieht nun wie folgt aus:
Nach einem Klick auf Deploy rechts oben speichert und übernimmt die Änderungen. Nun ist der Web-Server aktiv und lauscht auf Anfragen, wie ein Test mit curl
ergibt:
1$ curl -X POST http://<node-red-ip-adresse>:1880/intent
Der Server wird auf diese Anfrage derzeit noch nicht antworten - ein Timeout ist also die Folge
In der Node-RED-Oberfläche befindet sich rechts ein Käfer-Symbol. Nach einem Klick hierauf werden die Debug-Nachrichten ausgegeben - wahlweise für alle Flows oder nur den aktuellen. Hier ist nun die eben erfolgte Anfrage zu erkennen:
Der Payload (Inhalt) unserer Nachricht ist leer - doch wie sieht es aus, wenn Rhasspy einen Intent auslöst?
Sentences
Hierzu müssen wir ersten einen Schritt vorher anfangen. Am Anfang des Artikels haben wir gelernt, dass Rhasspy versucht erkannte Worte und Satzfragmente den einzelnen Kommandos zuzuordnen. Das bedeutet, dass ein solches Kommando mit denkbaren Formulieren noch zu erstellen ist. Ein Klick auf Sentences in der Rhasspy-Oberfläche öffnet einen entsprechenden Dialog mit einfachen und fortgeschrittenen Beispielen. Er wird um die folgenden Zeilen erweitert:
1[GetTime]
2wie spät ist es
3welcher tag ist heute
4sag mir die uhrzeit
5wie viel uhr haben wir
6welchen tag haben wir
Hier wird ein Kommando GetTime
mit verschiedenen Formulierungen definiert - ohne Satzzeichen und in Kleinbuchstaben. Nach einem Klick auf Save Sentences und Train steht die neue Funktionalität zur Verfügung.
Nach einem Klick auf Speech kann das Kommando auf mehrere Wege ausgeführt werden. Eine Möglichkeit ist es natürlich, den Assistent mithilfe des Rufworts anzusprechen. Eine andere ist die Aufnahme eines Kommandos über den Browser durch Klicken von Hold to Record oder Tap to Record. Eine dritte ist das Einfügen einer gültiger Formulierung neben Sentence und Klicken von Get Intent.
In jedem Fall sollte innerhalb Node-RED die Debug-Ausgabe nun einen wesentlichen umfangreicheren Payload enthalten. Dieser ist in JSON formuliert und wird von Node-RED direkt in Objekte bzw. Key/Value-Paare konvertiert.
Durch Zeigen auf einzelne Elemente der Nachricht können entweder die Pfade oder der Inhalt kopiert werden - beispielsweise um sie mit einem anderen Tool weiter verarbeiten zu können.
Von primärem Interesse sind die folgenden Payload-Elemente:
intent.name
- erkanntes Kommandotext
/raw_text
- erkannter Satztokens
- Array mit in Wörtern zerlegter Satz (nützlich für komplexere Kommandos)
Die erste Funktion
Auf Basis des erkannten Kommandos lassen sich nun Abzweigungen in Node-RED erstellen. Hierzu wird ein switch-Node aus der Werkzeugleiste in den Flow gezogen - der Eingang links wird mit dem Ausgang des http in-Nodes verbunden. Nach einem Doppelklick auf das neue Element wird die folgende Konfiguration vorgenommen:
Als Eingabeparameter dient das erkannte Kommando - erreichbar über den Pfad msg.payload.intent.name
. Über den Dialog lassen sich verschiedene Bedingungen definieren - in diesem Fall:
- Wenn die Eigenschaft "GetTime" entspricht führe 1 aus
- Ansonsten führe 2 aus
Die zweite Bedingung dient dazu, den Anwender darüber zu informieren, dass das Kommando nicht erkannt wurde. Es soll eine einfache Fehlermeldung zurückgeben, damit der Anwender weiß, dass er genuschelt hat. Die Meldung wird dann von Rhasspy vorgelesen. Um diese Funktion zu implementieren, ziehen wir ein function-Node rechts neben den Switch-Node und verknüpfen den 2.Ausgang (Name wird beim Hovern angezeigt) mit dem Eingang des neuen Nodes. Durch einen Doppelklick wird wieder die weitere Konfiguration vorgenommen:
- Name: Unrecognized
- Property:
msg.payload
- Format: Plain text
- Output as: Parsed JSON
Die JSON-Vorlage sieht wie folgt aus:
1{
2 "data": {
3 "intent": {
4 "name": "unrecognized",
5 "confidence": 0
6 }
7 },
8 "speech": {
9 "text": "Kommando nicht erkannt."
10 }
11}
Rhasspy erwartet als Antwort zwingend ein data
-Dictionary mit dem dazugehörigen, ausgeführten Intent. Das speech
-Dictionary ist optional, es definiert den vorzulesenden Text mithilfe des text
-Werts. In diesem Fall enthält der Wert eine kurze Fehlermeldung.
Nachdem die Antwort vorformuliert wurde, muss diese noch zurück an den anfragenden Client gesendet werden - hierzu wird ein http response-Node in die Arbeitsfläche gezogen. Dieser Node-Typ ist nur in Kombination mit dem http in-Node gültig, da es die Adresse des Clients und weitere Informationen aus diesem Node bezieht. Die Ausgabe des function-Nodes wird nun mit dem Eingang des response-Nodes verbunden. Nach einem Doppelklick werden die folgenden Einstellungen übernommen:
- Name: Send answer
- Headers:
- Content-Type:
application/json
- Content-Type:
Der Content-Type ist essentiell, da ansonsten die Kommunikation zwischen Rhasspy und Node-RED nicht sauber funktioniert. Rhasspy sendet eine in JSON encodierte Anfrage und erwartet daher auch eine solche Rückantwort.
Bisher sieht der Flow wie folgt aus:
Nun fehlt noch die erste Funktion zum Ausgeben der Uhrzeit/des Datums. Hierfür wird ein function-Node benötigt, der in die Arbeitsfläche gezogen und mit dem anderen Beinchen des Switch-Nodes verknüpft wird. Nach erfolgtem Doppelklick werden die folgenden Einstellungen übernommen:
- Name: Tell date/time
- Outputs:
1
Der Output muss auf 1
gesetzt werden, da die Funktion einen fertigen Satz mithilfe von JavaScript zusammenbauen soll, der dann in ein JSON-Template integriert und an den Client zurückgesendet werden soll.
Der Code sieht wie folgt aus:
1// get current time and date
2now = new Date();
3
4// get day, month, year
5day = now.getDate();
6month = now.toLocaleString('default', { month: 'long' });
7year = now.getFullYear();
8hours = now.getHours();
9minutes = now.getMinutes();
10
11// set-up and return string
12msg.payload = "Heute ist der " + day + ". " + month + " " + year + " und es ist " + hours + " Uhr " + minutes + ".";
13return msg;
Neben den function-Node ist nun wieder ein template-Node zu setzen, Ausgang und Eingang müssen verknüpft werden. Die Einstellungen hierfür sind auf die folgenden Werte zu setzen:
- Name: Intent response
- Property:
msg.payload
- Format: Mustache template
- Output as: Parsed JSON
Der Inhalt des Templates ist diesmal etwas umfangreicher:
1{
2 "intent": {
3 "name": "GetTime",
4 "confidence": 0
5 },
6 "speech": {
7 "text": "{{ payload }}"
8 },
9 "wakeId": "",
10 "siteId": "default",
11 "time_sec": 0.010800838470458984
12}
Klar erkennbar ist, dass speech.text
diesmal nicht fest kodiert wird sondern per Platzhalter definiert wird. Hierfür ist zwingend notwendig, dass Mustache template als Format festgelegt wurde. Neu sind auch die Parameter wakeID
, siteID
und time_sec
. Letzteres gibt an, wie lange die Funktion benötigt hat, um die Anfrage auszuführen. Ich habe der Eigenschaft einen willkürlichen Wert zugewiesen, da ich keinen Mehrwert in der Information sehe. Die Bedeutung der Werte wakeId
und siteID
sind mir bisher auch noch nicht bekannt - sie erscheinen in der ursprünglichen Rhasspy-Anfrage. Lässt man die Werte weg, bricht Rhasspy mit einer Fehlermeldung ab und der Intent wird nicht ausgeführt.
Zu guter Letzt fehlt noch die Verknüpfung zwischen Template-Ausgang und response-Eingang. Der Flow sieht nun wie folgt aus:
Nach einem Klick auf Deploy steht der Handler zur Verfügung, wie ein Test mit curl
zeigt:
1$ curl -H "Content-Type: application/json" -X POST -d '{"intent": {"name": "GetTime"}}' http://<node-red-ip-adresse>:1880/intent
2{"intent":{"name":"GetTime","confidence":0},"speech":{"text":"Heute ist der 26. April 2020 und es ist 9 Uhr 51."},"wakeId":"","siteId":"default","time_sec":0.010800838470458984}
Die curl-Parameter -H
und -d
sind notwendig um JSON-Header und -Payload zu setzen, da der Handler nun das Rhasspy-typische Anfrage-Format erwartet.
Zeit für einen Test! Mithilfe einer der drei Möglichkeiten wird der Intent ausgelöst - und binnen weniger Sekunden sollte eine Antwort vorgelesen werden:
Herzlichen Glückwunsch - der Assistent hat soeben seine erste Funktion erlernt! 🙂
Ausblick
In diesem Artikel haben wir zwei essentielle Bestandteile des Sprachassistenten miteinander verknüpft und unser Assistent hat seine erste Funktion erlernt. Im nächsten Teil beschäftigen wir uns mit dem Auslesen von Temperatur-Sensoren und Wetteransagen über das Internet.