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.

Ablauf einer Anfrage des Sprachassistenten

Note

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.

Talk to the mic!

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:

Download der Porcupine-Bibliothek und -Sprachmuster

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:

Alternative Sounds

Note

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://:1880/intent

Konfigurieren eines Remote Intent-Handlers

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:

MethodePOST
Datei-Uploads aktivieren?Nein
URL/intent
NameDetect 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:

Der erste Handler-Entwurf

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
Note

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:

Eine leere Nachricht wurde empfangen

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.

Intent-Request

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 Kommando
  • text / raw_text - erkannter Satz
  • tokens - 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:

Der Switch-Node führt Aktionen je nach Kommando aus

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:

  1. Wenn die Eigenschaft "GetTime" entspricht führe 1 aus
  2. Ansonsten führe 2 aus

JSON-Template für nicht erkannte Intents

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

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:

Unvollständiger Intent-Handler

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:

Einfacher Intent-Handler

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:

Der erste erfolgreich ausgeführte Intent

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.

Posts in this Series

Übersetzungen: