Raspberry Pi Sprachsteuerung mit Node-RED, voice2json und openHAB 2
In diesem Beitrag erstellen wir eine Raspberry Pi Sprachsteuerung mit Node-RED. Ihr erfahrt welche Hardware und Software benötigt wird. Viel Spaß mit dieser Schritt für Schritt Anleitung.
Raspberry Pi Sprachsteuerung mit Node-RED – Hardware
Als Hardware für die Steuerung empfehle ich ein Raspberry Pi 4 und ein Respeaker Mic 2.0 Mirkofon.
Raspberry Pi Sprachsteuerung mit Node-RED – Software
Software – Node-RED installieren
Als erstes müsst ihr Node-RED auf dem Raspberry Pi installieren. Folgt hierzu einfach dem Beitrag Node-RED auf Raspberry Pi installieren.
Ich gehe davon aus, dass ihr weiterhin per ssh auf dem Raspberry Pi eingeloggt seid. Falls nicht loggt euch bitte ein.
Plant ihr den Raspberry Pi mittels WLAN einzubinden. Hierfür über den Befehl sudo raspi-config
das Menü aufrufen und Network Options, Wireless LAN wählen. Dort durchläuft einfach die einzelnen Schritte. Ist im Grunde selbsterklärend.
Im nächsten Schritt stellt die Sprache ein. Wir benötigen dies für eine deutsche Sprachsteuerung.
- Wählt Punkt 4. „Localisation Options Set up language …“
- Nun den Unterpunkt I1 „Change Locale Set up language …“
- Hier müsst ihr eine ganze weile Pfeil nach unten drücken bis ihr den Eintrag „de_DE.UTF-8 UTF-8“ findet. Dies mit der Leertaste markieren, Tab drücken, Enter und einfach weiter durch navigieren.
Software – Voice2JSON installieren
Als weitere Komponeten benötigen wir Voice2JSON. Ruft die folgende Seite auf.
http://voice2json.org/install.html#debian-package
Hier wählt das entsprechende Paket und macht einen Rechtsklick (1) auf den Link. Im Kontextmenü (2) „Adresse des Links kopieren“ wählen. Und ja, armhf ist richtig, auch wenn arm64 den PI 3+,4 listet.
Nun auf dem Pi wget
gefolgt von der kopierten Adresse eingeben. In unserem Fall (Pi4) sieht dies wie folgt aus.
wget https://github.com/synesthesiam/voice2json/releases/download/v2.0/voice2json_2.0_armhf.deb
Nun entpacken wir das Paket mit folgendem Befehl und beantwortet alle Fragen mit „Y“.
sudo apt install ./voice2json_2.0_armhf.deb
Sprache herunterladen unter folgendem Link. Da Kaldi aktuell die beste Kombination aus Erkennung und Performance bietet, laden wir das Paket de_kaldi-zamia-2.0.tar.gz herunter.
http://voice2json.org/#supported-languages
wget https://github.com/synesthesiam/de_kaldi-zamia/archive/v2.0.tar.gz
Entpacken der Sprachdateien.
tar -xzvf v2.0.tar.gz
Software – SoX installieren
Um mit Sound zu arbeiten benötigen wir noch Sox (Sound-Exchange). Dies könnt ihr bei Debian einfach über folgenden Befehl installieren.
sudo apt-get install sox
Software – Node-RED Raspberry Pi Sprachsteuerung Nodes
Nun benötigen wir noch die Nodes der für die Sprachsteuerung. Installiert als erstes GIT
sudo apt-get install git
Zur Installation der Nodes wechselt in das Node-RED Verzeichnis
cd $HOME/.node-red
Im Anschluss installiert ihr die folgenden beiden nodes. Diese Nodes hat übrigens unser Mitglied Johannes von myfreelife e.V. erstellt. Danke Johannes für diese schlanke, hervorragende Lösung!
npm install johanneskropf/node-red-contrib-sox-utils
npm install johanneskropf/node-red-contrib-voice2json
Startet node-RED per node-red-start
und als bestes per Service.
sudo systemctl enable nodered.service
Node-RED Sprachsteuerung Nodes
Verbindet nun das Respeaker USB Mirko per Kabel mit dem USB Port des Raspberry Pi. Alle LEDs sollten direkte anfangen zu leuchten und bereits auf Sprache reagieren.
Hinweis: Die folgenden Sprachsteuerung beruht auf openHAB 2 als Smarthome Server. Falls ihr andere Lösungen wie ioBroker, FHEM, Home Assistant und weiteres nutzt, müsst ihr den Flow entsprechend anpassen.
Bei der Erstellung des Flows hat mich Johannes unterstützt. Damit ihr es einfacher habt, kopiert euch einfach den Flow in den Zwischenspeicher.
[
{
"id": "db234a3a.20fea8",
"type": "subflow",
"name": "RestItemOutBlank",
"info": "",
"category": "Openhab",
"in": [
{
"x": 80,
"y": 60,
"wires": [
{
"id": "ea72e050.af37c"
}
]
}
],
"out": [
{
"x": 520,
"y": 60,
"wires": [
{
"id": "11d6666a.4dbdaa",
"port": 0
}
]
}
],
"env": [
{
"name": "Url",
"type": "str",
"value": "http://localhost:8080",
"ui": {
"label": {
"en-US": "Openhab Url"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
}
],
"color": "#C0DEED",
"icon": "font-awesome/fa-sign-out",
"status": {
"x": 840,
"y": 120,
"wires": [
{
"id": "4740b8d8.517638",
"port": 0
},
{
"id": "8fc663ec.2924f",
"port": 0
}
]
}
},
{
"id": "ea72e050.af37c",
"type": "function",
"z": "db234a3a.20fea8",
"name": "RequestVars",
"func": "const url = env.get(\"Url\");\nmsg.url = url + \"/rest/items/\" + msg.item + \"/\";\nmsg.headers = {};\nmsg.headers['Content-Type'] = 'text/plain';\nmsg.headers['Accept'] = 'application/json';\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"x": 210,
"y": 60,
"wires": [
[
"11d6666a.4dbdaa"
]
]
},
{
"id": "11d6666a.4dbdaa",
"type": "http request",
"z": "db234a3a.20fea8",
"name": "",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"x": 390,
"y": 60,
"wires": [
[
"3d5a938e.bf762c"
]
]
},
{
"id": "3d5a938e.bf762c",
"type": "switch",
"z": "db234a3a.20fea8",
"name": "",
"property": "statusCode",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "200",
"vt": "num"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 550,
"y": 120,
"wires": [
[
"4740b8d8.517638"
],
[
"8fc663ec.2924f"
]
]
},
{
"id": "4740b8d8.517638",
"type": "trigger",
"z": "db234a3a.20fea8",
"name": "",
"op1": "{\"fill\":\"green\",\"shape\":\"dot\"}",
"op2": " ",
"op1type": "json",
"op2type": "str",
"duration": "1",
"extend": true,
"units": "s",
"reset": "",
"bytopic": "all",
"outputs": 1,
"x": 700,
"y": 120,
"wires": [
[]
]
},
{
"id": "8fc663ec.2924f",
"type": "trigger",
"z": "db234a3a.20fea8",
"name": "",
"op1": "{\"fill\":\"red\",\"shape\":\"dot\"}",
"op2": " ",
"op1type": "json",
"op2type": "str",
"duration": "1",
"extend": true,
"units": "s",
"reset": "",
"bytopic": "all",
"outputs": 1,
"x": 700,
"y": 180,
"wires": [
[]
]
},
{
"id": "9f69ee30.2ec97",
"type": "tab",
"label": "LocalVoice",
"disabled": false,
"info": ""
},
{
"id": "18ca7c6e.a2e434",
"type": "sox-record",
"z": "9f69ee30.2ec97",
"name": "",
"buttonStart": "msg",
"inputs": 1,
"inputSource": "1,0",
"byteOrder": "-L",
"encoding": "signed-integer",
"channels": 1,
"rate": 16000,
"bits": 16,
"gain": "0",
"lowpass": 8000,
"showDuration": false,
"durationType": "forever",
"durationLength": 0,
"silenceDetection": "nothing",
"silenceDuration": "2.0",
"silenceThreshold": "2.0",
"outputFormat": "stream",
"manualPath": "",
"debugOutput": false,
"x": 150,
"y": 280,
"wires": [
[
"47a1e6ee.4462d8"
],
[]
]
},
{
"id": "47a1e6ee.4462d8",
"type": "voice2json-wait-wake",
"z": "9f69ee30.2ec97",
"name": "",
"voice2JsonConfig": "fd76f088.e4c66",
"inputField": "payload",
"controlField": "control",
"outputField": "payload",
"nonContinousListen": true,
"x": 380,
"y": 280,
"wires": [
[
"a718728f.487f4",
"eea2d822.4011a8"
],
[
"2f0150b9.dd289"
]
]
},
{
"id": "a718728f.487f4",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 610,
"y": 220,
"wires": []
},
{
"id": "c81bee83.d3e09",
"type": "voice2json-training",
"z": "9f69ee30.2ec97",
"name": "",
"voice2JsonConfig": "fd76f088.e4c66",
"inputField": "payload",
"outputField": "payload",
"loadedProfile": "",
"x": 370,
"y": 500,
"wires": [
[
"83dedab3.c18eb8",
"85de8340.ac046",
"3d6e07c7.254a78"
]
]
},
{
"id": "16f0a80.dbb9d58",
"type": "inject",
"z": "9f69ee30.2ec97",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "train",
"payloadType": "str",
"x": 160,
"y": 500,
"wires": [
[
"c81bee83.d3e09",
"38ded18a.d4105e"
]
]
},
{
"id": "83dedab3.c18eb8",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 590,
"y": 500,
"wires": []
},
{
"id": "85de8340.ac046",
"type": "file",
"z": "9f69ee30.2ec97",
"name": "",
"filename": "/home/pi/training.txt",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 610,
"y": 540,
"wires": [
[]
]
},
{
"id": "2f0150b9.dd289",
"type": "voice2json-transcribe-stream",
"z": "9f69ee30.2ec97",
"name": "",
"voice2JsonConfig": "fd76f088.e4c66",
"inputField": "payload",
"outputField": "payload",
"x": 670,
"y": 280,
"wires": [
[
"287e72ff.75511e",
"44a220e4.28b55"
]
]
},
{
"id": "4047cdae.b3f6f4",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1530,
"y": 220,
"wires": []
},
{
"id": "287e72ff.75511e",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "",
"rules": [
{
"t": "set",
"p": "control",
"pt": "msg",
"to": "listen",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 670,
"y": 380,
"wires": [
[
"47a1e6ee.4462d8"
]
]
},
{
"id": "44a220e4.28b55",
"type": "voice2json-tti",
"z": "9f69ee30.2ec97",
"name": "",
"voice2JsonConfig": "fd76f088.e4c66",
"inputField": "payload.text",
"controlField": "control",
"outputField": "payload",
"autoStart": true,
"x": 940,
"y": 280,
"wires": [
[
"ff198956.161b68",
"de126189.fc94f"
]
]
},
{
"id": "22ea7572.9f805a",
"type": "inject",
"z": "9f69ee30.2ec97",
"name": "",
"props": [
{
"p": "control",
"v": "start",
"vt": "str"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 900,
"y": 380,
"wires": [
[
"44a220e4.28b55"
]
]
},
{
"id": "bffdb5b4.05a0c8",
"type": "subflow:db234a3a.20fea8",
"z": "9f69ee30.2ec97",
"name": "openHAB",
"env": [
{
"name": "Url",
"value": "http://oh2:8080",
"type": "str"
}
],
"x": 1560,
"y": 280,
"wires": [
[
"4e30d3e6.e58c4c",
"2233ac63.7b7814"
]
]
},
{
"id": "3a27470d.f15c48",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "",
"rules": [
{
"t": "move",
"p": "payload",
"pt": "msg",
"to": "original",
"tot": "msg"
},
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "original.slots.command",
"tot": "msg"
},
{
"t": "set",
"p": "item",
"pt": "msg",
"to": "original.slots.item",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1360,
"y": 280,
"wires": [
[
"4047cdae.b3f6f4",
"bffdb5b4.05a0c8"
]
]
},
{
"id": "4e30d3e6.e58c4c",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1710,
"y": 280,
"wires": []
},
{
"id": "68ab16fa.60cc78",
"type": "inject",
"z": "9f69ee30.2ec97",
"name": "",
"props": [
{
"p": "control",
"v": "stop",
"vt": "str"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payloadType": "str",
"x": 160,
"y": 360,
"wires": [
[
"47a1e6ee.4462d8"
]
]
},
{
"id": "eea2d822.4011a8",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "TonSpielen",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "bing1.wav",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 630,
"y": 120,
"wires": [
[
"5f8094bf.4bb46c"
]
]
},
{
"id": "2233ac63.7b7814",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "bing2.wav",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "bing2.wav",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1720,
"y": 140,
"wires": [
[
"5f8094bf.4bb46c"
]
]
},
{
"id": "b5e2e51f.eec2a8",
"type": "comment",
"z": "9f69ee30.2ec97",
"name": "Local Speech",
"info": "",
"x": 130,
"y": 80,
"wires": []
},
{
"id": "ff198956.161b68",
"type": "switch",
"z": "9f69ee30.2ec97",
"name": "",
"property": "payload.intent.name",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "Licht",
"vt": "str"
},
{
"t": "eq",
"v": "beispiel",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 1150,
"y": 280,
"wires": [
[
"3a27470d.f15c48"
],
[
"de126189.fc94f"
]
]
},
{
"id": "de126189.fc94f",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "false",
"statusVal": "",
"statusType": "auto",
"x": 1350,
"y": 360,
"wires": []
},
{
"id": "c36db8a4.723f18",
"type": "inject",
"z": "9f69ee30.2ec97",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": "1",
"topic": "",
"payload": "start",
"payloadType": "str",
"x": 110,
"y": 140,
"wires": [
[
"18ca7c6e.a2e434"
]
]
},
{
"id": "5f8094bf.4bb46c",
"type": "sox-play",
"z": "9f69ee30.2ec97",
"name": "",
"outputDevice": "0,0",
"gain": "20",
"startNew": "queue",
"debugOutput": true,
"x": 1720,
"y": 60,
"wires": [
[]
]
},
{
"id": "38ded18a.d4105e",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "stop",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 370,
"y": 420,
"wires": [
[
"18ca7c6e.a2e434"
]
]
},
{
"id": "3d6e07c7.254a78",
"type": "change",
"z": "9f69ee30.2ec97",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": " sudo systemctl restart nodered.service",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 610,
"y": 600,
"wires": [
[
"ba4c88dd.cfb568"
]
]
},
{
"id": "ba4c88dd.cfb568",
"type": "exec",
"z": "9f69ee30.2ec97",
"command": "",
"addpay": true,
"append": "",
"useSpawn": "false",
"timer": "",
"oldrc": false,
"name": "",
"x": 800,
"y": 600,
"wires": [
[],
[
"94d883bc.cee93"
],
[]
]
},
{
"id": "94d883bc.cee93",
"type": "debug",
"z": "9f69ee30.2ec97",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 970,
"y": 600,
"wires": []
},
{
"id": "fd76f088.e4c66",
"type": "voice2json-config",
"z": "",
"profilePath": "/home/pi/de_kaldi-zamia-2.0",
"name": "Kaldi_Deutsch",
"sentences": "[Licht]\ncommands = (an:ON | ein:ON | einschalten:ON | anschalten:ON | aus:OFF | ausschalten:OFF){command}\n($items) (<commands>)\nschalte ($items) [(an:ON | aus:OFF){command}]",
"slots": [
{
"fileName": "items",
"managedBy": "nodered",
"fileContent": "([die] dachboden lampe){item: dg_l_lampe}\n([das] licht auf dem dachboden){item: dg_l_lampe}\n([das] dachboden licht){item: dg_l_ring}\n([den] ledring auf dem dachboden){item: dg_l_ring}\n(wohnzimmer lampe){item: dg_l_lampe}\n(schlafzimmer lampe){item: dg_l_ring}",
"executable": false
}
],
"removeSlots": true
}
]
Im Anschluss geht in die Node-RED Oberfläche und importiert den Flow. Klickt auf das Menü Icon (1) und wählt Import (2).
Fügt den kopierten Flow in die Box (1) ein und drückt Import (2).
Als nächstes müsst ihr einige wenige Einstellungen vornehmen.
Node-Red Sprachsteuerung Konfiguration
Nun müsst ihr die Konfiguration noch auf eure Bedürfnisse hin anpassen.
Node-Red Sprachsteuerung Konfiguration – openHAB Host
Wenn ihr wie ich openHAB nutzt, ersetzt als erstes den Hostnamen. Öffnet die Node (1), ändert den Host (2) auf euren Hostnamen und klickt Fertig (3). Im Anschluss werden die Befehle an die Rest API von openHAB 2 gesendet.
Node-Red Sprachsteuerung Konfiguration – Kaldi
Als erstes passt die Kaldi Konfiguration an. Klickt auf die Node (1) und im Anschluss auf den ändern Knopf (2).
Hier passt den Pfad zum Kaldi Verzeichnis (1) an. Hier könnt ihr weiterhin einzelne Intents anlegen und die Steuerung für die Sprachbefehle definieren (2).
Eine detaillierte Erklärung zur Syntax gibt es im voice2json whitepaper.
Nun ist es noch wichtig, dass ihr die Slots aufruft (1) und mit dem Bleistift (2) die Konfiguration aufruft und an eure Bedarfe anpasst (3). Zur Erklärung ein Beispiel anhand der Zeilen vorhanden Beispielzeilen. In den runden Klammern steht der Text wie ich das Gerät ansprechen möchte. Ich kann also dachboden lampe, licht auf dem dachboden usw. sagen. In eckigen Klarmmern steht ein optionales Wort. So ist es egal, ob ich das dachboden licht oder dachboden licht sage. In den geschweiften Klammern kommt der Item Namen rein.
Klickt nun oben rechts in der Node-RED UI auf deploy damit eure bisherigen Änderungen aktiviert werden.
Node-Red Sprachsteuerung Konfiguration – Training
Ein weitere wichtige Komponente ist das Training. Nach jeder Änderung der Kaldi Konfiguration müsst ihr ein Training durchführen und Node-RED neu starten. Ich habe den Flow daher so optimiert, dass die nötigen Schritte direkt ausgeführt werden. Klickt einfach auf train (1) und die Schritte laufen automatisiert ab.
Wartet einen Augenblick und ihr die Erkennung sollte funktionieren. Viel Spaß mit dem Ausbau eurer Konfiguration.
Es klappt was nicht? Lasst ein Kommentar da. Ich gebe die Kommentare manuell frei, daher nicht wundern, wenn es etwas dauert. 🙂