Im letzten Teil dieser Serie haben wir dem Sprachassistenten das Auslesen von Temperatursensoren und die Wetteransage über das Internet beigebracht. In diesem Artikel kommt die Unterhaltung mit einem selbstgebauten Internet-Radio und einer API für schlechte Witze nicht zu kurz.
Radio-Streams unter Linux
Eine beliebte Funktion fertiger Sprachassistenten ist das Abspielen von Musik und Internet-Radio – eine solche Funktion darf in einem Eigenbau natürlich nicht fehlen (nicht zuletzt um den WAF des Bastelprojekts zu erhöhen). Eine solche Funktion liefert Rhasspy nicht mit, was aber nicht weiter stört – immerhin eröffnet der Einsatz von Node-RED weitere Integrationsmöglichkeiten. So können wir beispielsweise komfortabel weitere Container ansprechen und fernsteuern.
Um unter Linux in den Genuss von Internet-Radio zu kommen gibt es zahlreiche Tools – eines davon ist der vielseitige Player mplayer, der mit einer angeführten URL in der Regel sofort die entsprechende Station abspielt:
$ mplayer http://rbb-fritz-live.cast.addradio.de/rbb/fritz/live/mp3/128/stream.mp3
Einige Stationen verbergen die tatsächliche MP3/AAC-URL in einer Playlist (*m3u, *.pls) – mplayer kann damit oftmals nicht umgehen und bricht ab. Hier hilft der Einsatz von curl, um die Weiterleitung herauszufinden:
$ curl http://streams.ffh.de/radioffh/mp3/hqlivestream.m3u
http://mp3.ffh.de/radioffh/hqlivestream.mp3
Einfacher Container
Schön und gut – jetzt könnte man innerhalb Node-RED Befehle erstellen, um entsprechende Kommandos auf dem Host auszuführen. Hier wäre es aber auch wieder notwendig, einen Benutzer zu erstellen, den Login per SSH (oder einem ähnlichen Protokoll) zu implementieren. Pro Station könnte man ein entsprechendes Kommando erstellen. Das muss doch irgendwie schöner gehen…
Ich dachte hier an eine API, die Node-RED einfach direkt ansprechen kann, um einfache Befehle abzusetzen (Starte Radio, wechsele Sender, etc.). Da ich öfter in Python programmiere, fiel die Wahl schnell auf Flask – ein schlankes, aber dennoch leistungsfähiges Framework für Webservices und APIs. Folgende Funktionen wollte ich an Board haben:
- Speichern der Radiostationen in einer SQLite-Datenbank
- REST API und rudimentäre UI für die Pflege und Steuerung von Stationen
- Steuerung der Lautstärke (über amixer)
An einem Nachmittag mit stark ausgeprägter Langeweile ist hier eine rudimentäre App entstanden, die sich auf GitHub findet. Vorgefertigte Container auf Basis von Ubuntu für x86_64 und ARM stelle ich auf Docker Hub zur Verfügung.
Die Anwendung stellt verschiedene API-Calls zur Verfügung, mit welchen die Funktionen gesteuert werden – beispielsweise:
Aufruf | Methode | Funktion |
/api/stations | POST | Neuen Sender einspeichern |
/api/stations/<id>/<name>/play | GET | Sender abspielen |
/api/next | GET | Nächster Sender |
/api/previous | GET | Vorheriger Sender |
/api/stop | GET | Radio stoppen |
/api/volume | POST | Lautstärke ändern |
Für API-Faule gibt es eine rudimentäre Web-Oberfläche:
Im GitHub-Repository findet sich auch eine Konfigurationsdatei für docker-compose:
version: "3"
services:
radio:
container_name: radio_api
image: stdevel/radio_api:latest
ports:
- "5000:5000"
devices:
- "/dev/snd:/dev/snd"
volumes:
- data:/opt/radio_api/instance
restart: unless-stopped
volumes:
data:
Die Anwendung lauscht auf TCP-Port 5000, für die Datenbank der Radio-Stationen wird ein Volume erstellt. Die Gerätedatei /dev/snd wird an den Container weitergeleitet.
Der Container wird wie folgt erstellt und gestartet:
$ docker-compose up -d
Henne-Ei-Problem
Das grosse Problem an der Sache ist, dass sich Rhasspy und radio_api die Soundkarte teilen. Das bedeutet konkret, dass Sprachkommandos nicht zuverlässig funktionieren, während das Radio läuft. Das Rufwort wird gelegentlich fälschlicherweise ausgelöst und eingesprochene Kommandos werden oft aufgrund der Hintergrundmusik nicht erkannt. Ein Umweg ist es natürlich, das Radio über die Web-Oberfläche zu deaktivieren bevor das nächste Kommando eingesprochen wird.
Hier habe ich noch keine elegante Lösung gefunden – über Anregungen und Tipps bin ich an dieser Stelle dankbar. Ein für mich funktionaler Workaround ist jedoch der Einsatz von MQTT in Kombination mit einer Smartphone-App wie IoT OnOff (iOS) bzw. MQTT Dash (Android). Das Betätigen eines Knopfs auf dem Smartphone ist für mich einfacher als das Aufrufen einer Webseite, die nicht für Smartphones optimiert ist. Innerhalb Mosquitto habe ich den MQTT-Benutzer operator in der ACL um folgende Topics erweitert:
# operator
user operator
topic read #
topic read $SYS/#
topic readwrite radio/status
topic readwrite radio/station
topic readwrite radio/volume
Node-RED lauscht in einem Flow auf die Topics und steuert das Radio per API, sobald Kommandos eingehen:
Payload | Topic | Beschreibung |
stop | radio/status | Stoppt das Radio |
prev | Vorheriger Sender | |
next | Nächster Sender | |
<int> | radio/volume | Lautstärke ändern (0 – 100%) |
Ein entsprechendes Dashboard auf dem Smartphone steuert so Lautstärke und Radiosender:
Der passende Flow ist auf GitHub verfügbar.
Schlechte Witze as a Service
Ich bin ein grosser Freund schlechter (Wort)witze – also lag es nahe, hierfür eine API zu entwickeln und mit dem Sprachassistenten zu verknüpfen. Das sorgt auf Partys entweder für grossen Spaß oder Fremdscham – eine schmale Gratwanderung.
Hier habe ich den Code der Radio-API wiederverwendet – folgende Anforderungen empfand ich als wichtig:
- Bereitstellen verschiedener Kategorien (normale Witze, schlechte Witze, Filmzitate,…)
- Speichern von Witzen in Kategorien in einer SQLite-Datenbank
- Zufallsmodus
- REST-Calls und rudimentäre UI für die Pflege von Kategorien und Witzen
Gesagt, getan – daraus ist an einem langen Wochenende eine fertige App geworden, die sich auf GitHub findet. Auch hier gibt es wieder vorgefertigte Container auf Basis von Alpine Linux für x86_64 und ARM auf Docker Hub.
Die entsprechenden API-Calls finden sich in der Dokumentation oder einer Postman-Collection – ein Auszug:
Aufruf | Methode | Funktion |
/api/categories | POST | Neue Kategorie erstellen |
/api/categories | GET | Kategorie-Informationen beziehen |
/api/jokes | POST | Neuen Witz speichern |
/api/jokes/random/ | GET | Zufälliger Witz |
/api/jokes/random/<id,name> | GET | Zufälliger Witz einer bestimmten Kategorie |
/api/jokes/random/<id,name>/<rank> | POST | Zufälliger Witz einer bestimmten Kategorie mit Mindest-Ranking |
Für die Pflege steht ebenfalls eine rudimentäre Web-Oberfläche bereit:
Im GitHub-Repository findet sich auch eine Konfigurationsdatei für docker-compose:
version: "3"
services:
joke_api:
container_name: joke_api
image: stdevel/joke-api:latest
ports:
- "5001:5000/tcp"
volumes:
- data:/opt/joke_api/instance
restart: unless-stopped
volumes:
data:
Die Applikation ist per TCP-Port 5001 erreichbar, für die Datenbank wird ein dediziertes Volume angelegt – dieses kann zwischen Updates dann entsprechend gesichert und wiederhergestellt werden (damit niemand auf schlechte Witze verzichten muss).
Der Container wird in nun gewohnter Manier erstellt und gestartet:
$ docker-compose up -d
Sentences
Damit Rhasspy die neuen Kommandos anwenden kann, müssen zwei entsprechende Sentences definiert werden:
[TellJoke]
erzähl einen witz
[PlayRadio]
radio an
spiele radio
schalte das radio ein
TellJoke bezieht einen Witz und liest diesen vor, während PlayRadio das Radio anschaltet. Entsprechende Kommandos zum Wechseln der Sender und Abschalten des Radios haben sich bei mir als nicht praxistauglich erwiesen, da diese wie oben beschrieben nicht zuverlässig funktionieren.
Mit einem Klick auf Train werden die Änderungen übernommen.
Verknüpfen mit Node-RED
Der erste Schritt besteht darin, in der Node-RED Oberfläche den Rhasspy-Handler zu öffnen und den Commands-Switch um zwei Fälle zu erweitern: TellJoke und PlayRadio.
Anschließend wird ein http request-Node in den Flow gezogen und mit dem TellJoke-Fall verbunden. Per Doppelklick werden die folgenden Einstellungen übernommen:
- Method: GET
- URL: http://localhost:5001/api/jokes/random
- Return: a parsed JSON object
- Name: Random joke
HINWEIS: Die URL bezieht einen zufälligen Witz aus einer beliebigen Kategorie. Falls nur eine bestimmte Kategorie ausgewählt werden soll, genügt das Anhängen des Kategorienamen, z.B. /generic
Bei Return ist wieder darauf zu achten, dass ein JSON-Objekt zurückgegeben wird. Dieses wird im nächsten Schritt weiter verarbeitet – es folgt ein Template-Node, dessen Eingang mit dem Ausgang des http request-Node verbunden wird. Die nachfolgenden Einstellugen sind zu übernehmen:
- Name: Intent response
- Format: Mustache template
- Output as: Parsed JSON
Auch hier sind das Template-Format und das zurückgegebene JSON-Objekt wieder wichtig – das eigentliche Template sieht wie folgt aus:
{
"intent": {
"name": "TellJoke",
"confidence": 0
},
"speech": {
"text": "{{ payload.results.0.joke_text }}"
},
"wakeId": "",
"siteId": "default",
"time_sec": 0.010800838470458984
}
Der speech.text-Eigenschaft wird der Text des zufällig bezogenen Witzes zugewiesen.
Abschliessend wird der Ausgang des Template-Objekts dem http reponse-Objekt zugewiesen.
Für den zweiten Fall wird erneut ein http request-Node erstellt und mit dem verbleibenden Fall verbunden. Dabei sind die folgenden Einstellungen zu übernehmen:
- Method: GET
- URL: http://localhost:5000/api/stations/1/play
- Return: a parsed JSON object
- Name: Play radio
Die URL zeigt zur Radio-API und startet dort den ersten eingespeicherten Radiosender. Senderwechsel werden danach über den vorhin beschriebenen Workaround vorgenommen.
Der Ausgang des Nodes wird mit einem weiteren Template-Node verbunden, welches die folgenden Anpassungen erfährt:
- Name: Intent response
- Format: Mustache template
- Output as: Parsed JSON
Das Template enthält diesmal keinen auszugebenden Text:
{
"intent": {
"name": "PlayRadio",
"confidence": 0
},
"wakeId": "",
"siteId": "default",
"time_sec": 0.010800838470458984
}
Zuletzt wird der Ausgang des Templates mit dem http response-Node verbunden, die Änderungen werden mit einem Klick auf Deploy gespeichert.
Die Funktionen stehen nun zur Verfügung – Zeit für einen Test; entweder per curl oder Verwendung des Sprachkommandos.
$ curl -H "Content-Type: application/json" -X POST -d '{"intent": {"name": "TellJoke"}}' http://<node-red-ip-adresse>:1880/intent
{"intent":{"name":"TellJoke","confidence":0},"speech":{"text":"Wie heißt einen Spanier ohne Auto? Carlos."},"wakeId":"","siteId":"default","time_sec":0.010800838470458984}
Ausblick
In diesem Artikel haben wir mit einem Online-Radio und schlechten Witzen am laufenden Band einiges für das Entertainment getan. Jedoch gibt es natürlich auch hier wieder Optimierungspotential.
Für einige mag es etwas unschön sein, dass wir nun schon vier Ports für die verwendeten Applikationen geöffnet haben. Eine elegantere Lösung wäre es, die einzelnen Ports hinter einen Reverse Proxy zu legen und die einzelnen Anwendungen per URL-Weiterleitung zugänglich zu machen – beispielsweise:
URL | Port |
/node-red | localhost:1880 |
/radio | localhost:5000 |
/jokes | localhost:5001 |
/rhasspy | localhost:12101 |
Hier wäre der Einsatz einer Software wie NGINX oder Traefik denkbar.