Spracherkennung im Web 28.06.2021, 10:10 Uhr

Grammatikbasierte Spracherkennung

Die Web-Variante von Picovoice erlaubt die Integration lokaler Spracherkennungsfunktionen in verschiedene JavaScript-Frameworks.
(Quelle: Foto: Viktorus  / Shutterstock)
Außer Frage steht, dass cloudbasierte Spracherkennungssysteme wie Microsofts Azure Cognitive Services exzellente Ergebnisse liefern. Allerdings geht ihre Nutzung erstens mit permanenten Mietkosten einher, zweitens ist man immer vom Server des Drittanbieters abhängig, was sowohl in Bezug auf Sanktionssicherheit als auch auf Verfügbarkeit und Datenschutz ein Problem darstellen kann.
Das Startup Picovoice hat sich die grammatikbasierte Spracherkennung ohne Internetverbindung auf die Fahnen geschrieben. Auf Mikrocontrollern und größeren Systemen ist Picovoice schon seit einiger Zeit vertreten, nun wagt man dank WebAssembly den Schritt ins Web (Bild 1).
Das Startup Picovoice bietet eine grammatikbasierte Spracherkennung (Bild 1)
Quelle: Hanna
Gleich zu Beginn sei angemerkt, dass das Produkt keine Alternative zu vollformartigen Spracherkennungsprogrammen wie Lernout und Hauspies Dragon darstellen will. Es handelt sich vielmehr um eine sogenannte grammatikbasierte Engine. Dahinter verbirgt sich der Gedanke, dass der Entwickler eine Art Liste aller jemals von der Engine zu verarbeitenden Ausdrücke anliefern muss. Anhand dieser Informationen ist die Engine dann in der Lage, eingehende Benutzereingaben zu erfassen - und dies mit überraschend hoher Genauigkeit. Neu ist die Verfügbarkeit der WebAssembly-Version der Engine, die aktuell die Browser Chrome, Edge, Firefox und Safari unterstützt.
Mehr zu Base64
Das Base64-Format ist auch abseits von Picovoice relevant. Eine empfehlenswerte Zusammenfassung findet sich unter https://developer.mozilla.org/en-US/docs/Glossary/Base64.
Im Bereich der Spracherkennung ist es oft nicht erforderlich, komplizierte Befehle zu verstehen. In vielen Fällen reicht es völlig aus, wenn der Computer eine Gruppe von Wörtern unterscheiden kann. Dieses Verfahren wurde beispielsweise in HPs Infinuum-Oszilloskop verwendet, um mit einem Prototypen interagierenden Ingenieuren das Anpassen der Einstellungen des Meßgeräts ohne Handbewegungen zu ermöglichen.

Wakeword-Erkennung

In der schönen neuen Welt der künstlichen Intelligenz spricht man in diesem Zusammenhang auch von Wakeword Detection. Ein Feature, das in der Welt der Picovoice-API durch eine als Porcupine bezeichnete Engine abgebildet wird. Für unsere ersten Gehversuche wollen wir (unter Ubuntu 20.04 LTS) ein Arbeitsverzeichnis namens picovoice anlegen.
Das Picovoice-Entwicklerteam unterstützt sowohl Yarn als auch npm. Wir wollen hier mit npm (Version 6.14.12) und der Node.js (14.16.1) arbeiten. Nach einem Versionsabgleich gegen die auf ihrer Workstation installierten Varianten der Kommandozeilenwerkzeuge können Sie auch schon dazu übergehen, in einem Unterordner (hier porcupinewebdemo) ein erstes Projektskelett anzulegen. Die von npm Init angebotenen Voreinstellungen dürfen sie dabei kommentarlos übernehmen:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$ npm init
Obwohl Picovoice ein vergleichsweise neues Produkt ist, stehen die Javascript-Dateien bereits in verschiedenen Content Delivery Networks zur Verfügung. Unsere erste Aktion besteht deshalb darin, die für die Erkennung deutscher Sprecher benötigten Module zu unserem Projektskelett hinzuzufügen:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$ npm install @picovoice/porcupine-web-de-worker @picovoice/web-voice-processor
. . .
+ @picovoice/web-voice-processor@2.0.2
+ @picovoice/porcupine-web-de-worker@1.9.3 . . .
Im Interesse kompakterer Auslieferung der Javascript-Dateien auf dem Client modularisiert Picovoice das vorliegende Produkt. Das Paket @picovoice/web-voice-processor ist für die Realisierung der Processing-Toolchain verantwortlich, die Daten via Mikrofon entgegennimmt und in ein für die eigentlichen Interferenzmodelle verständliches Format bringt.

Standard-Schlüsselwörter

Ein auf eine Gruppe von Standard-Schlüsselwörtern vortrainiertes Erkennungsmodell, das auf deutsche Akzente optimiert ist, findet sich neben der Erkennungslogik im Paket @picovoice/porcupine-web-de-worker. Beachten Sie, dass Picovoice aktuell auch einige andere Sprachen unterstützt. Wenn Sie diese ebenfalls in dem Projekt installieren möchten, müssen Sie natürlich auch die zu den jeweiligen Sprachvarianten gehörenden Interferenz-Modelle durch dedizierte Aufrufe von npm installieren. Wegen des modularen Aufbaus von npm lässt sich dies allerdings auch später problemlos nachholen.
Die eigentliche Arbeit wird Picovoice dann in einerHTML-Datei erledigen. Wer unter Linux programmiert, kann hier auf das touch-Kommandozeilenwerkzeug zurückgreifen:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$
touch index.htm
Die erste Aktion des HTML-Testprogramms besteht dann darin, die per npm install bereitgestellten Module zu laden. Freundlicherweise sind die Dateinamen der einzelnen Einsprungpunkt quer über das gesamte Picovoice-Portfolio identisch, weshalb der <head>-Teil unserer Datei folgendermaßen aussieht:

<html lang="en">
<head>
<script src="node_modules/@picovoice/porcupine-web-de-worker/dist/iife/index.js">
</script>
<script src="node_modules/@picovoice/
web-voice-processor/dist/iife/index.js">
</script>
</head>
Der hier abgedruckte Beispielcode lädt ein Interferenzmodell, das auf deutsche Aussprache und Grammatik trainiert ist. Möchten Sie bei Ihrem Test stattdessen eine andere Sprachversion verwenden, so müssen Sie den <script>-Tag nach folgendem Schema anpassen und die fehlenden Module nachinstallieren:

<script src="node_modules/@picovoice/
porcupine-web-en-worker/dist/iife/
index.js"></script>
<script src="node_modules/@picovoice/
porcupine-web-fr-worker/dist/iife/index.js"></script>
<script src="node_modules/@picovoice/
porcupine-web-es-worker/dist/iife/index.js"></script>
Das Abdrucken mehrerer Skript-Tags ist übrigens kein Druckfehler. Die Picovoice-Engine ist in sich so sehr modularisiert, dass auch mehrere Interferenz-Engines gleichzeitig in einer Webseite und sogar in einem Namespace leben können.
Als nächstes müssen wir uns darum kümmern, dass die von der Interferenz-Engine erzeugten Ereignisse für uns sichtbar werden. Die von Picovoice entwickelte Methode writeMessage ist dafür optimal geeignet, da sie ohne sonstige Anpassungen im <body> der HTML-Page platziert werden kann:
<body>
<script type="application/javascript">
function writeMessage(message) {
  console.log(message);
  let p = document.createElement("p");
  let text = document.createTextNode(message);
  p.appendChild(text);
  document.body.appendChild(p);
}
Die Methode writeMessage füttert zuerst die Entwickler-Kommandozeile, um danach ein neues <p>-Tag mit der als Parameter angelieferten Statusinformation zu bevölkern. Dieses wird danach als Kind an den Body angefügt.
Im nächsten Schritt müssen wir uns um die eigentliche Bereitstellung der Erkennungs-Infrastruktur kümmern. Wie im Fall vieler anderer AI-Frameworks gilt hier, dass das und Vorbereiten der Koeffizienten eine rechenintensivere Aufgabe ist. In JavaScript erledigt man dies am besten durch eine async-Funktion:
async function startPorcupine() {
    writeMessage("Porcupine is loading.Please wait...");
    let ppnEn = await PorcupineWebDeWorker.
    PorcupineWorkerFactory.create({
        builtin: "Ananas",
        sensitivity: 0.7,
    });
Jede der erwähnten sprachspezifischen Interferenz-Engines bringt einen eigenen Namespace mit. Für die deutsche Sprache ist der Namespace PorcupineWebDeWorker erforderlich, wie eine englische Variante in PorcupineWebEnWorker unterkäme. Im Rahmen der Erzeugung der Engine legen wir zudem fest, auf welche Wakeword die Engine zu hören hat. Das Attribut sensitivity erlaubt das Abstimmen des Sensitivitätsgrads. Die Engine kann empfindlicher reagieren, produziert in diesem Fall aber auch mehr Fehler.
Spracherkennungsengines arbeiten prinzipiell asynchron und parallel zum Rest der Web-Applikation. Im nächsten Schritt erzeugen wir deshalb eine Methode, die von Picovoice Web immer dann aufgerufen wird, wenn ein neues sprachbezogenes Ereignis bereitsteht:

const keywordDetection = (msg) => {
    if (msg.data.command === "ppn-keyword") {
        writeMessage("Keyword detected:
        " + msg.data.keywordLabel);
    }
};
ppnEn.onmessage = keywordDetection;
Picovoice generiert während der Sprachdatenverarbeitung Dutzende verschiedener Ereignisse, die wir anhand des Attributs msg.data.command unterscheiden. Wir prüfen hier im ersten Schritt auf den magischen String ppn-keyword, der die Erkennung eines Wakewords anmeldet. Die eigentlichen Wakeword-Informationen finden sich dann im Parameter msg.data.keywordLabel.
Interessant ist in diesem Zusammenhang noch, dass das eigentliche Anmelden des Eventhandlers durch Einschreiben in das Attribut ppnEn.onmessage erfolgt. Wer mehrere Interferenzmodelle parallel verwendet, muss jedem von ihnen einen Handler einschreiben. Ob sie alle vier Modelle mit einem gemeinsamen Handler ausstatten, oder für jede Sprachversion eine eigene Methode bevorzugen, bleibt ihren individuellen Präferenzen überlassen.
Im nächsten Schritt erfolgt die eigentliche Aktivierung der Toolchain zur Datenbereitstellung. Porcupine kümmert sich dabei netterweise auch darum, die in manchen Browsern erforderliche permission-Anfrage loszutreten und die Ereignisse zu verarbeiten:

try {
    let webVp = await window.WebVoiceProcessor.
    WebVoiceProcessor.init({engines: [ppnEn], });
    } catch (e) {
    writeMessage("WebVoiceProcessor failed to
    initialize: " + e);
    }
}
Zur lauffähigen ersten Version unserer Engine müssen wir dann nur noch dafür sorgen, dass die eigentliche Start-Funktion nach dem erfolgreichen Aufbau des Document Object Models aktiviert wird. Auch dies lässt sich - man glaubt es kaum - auch ohne jQuery bewerkstelligen:

document.addEventListener("DOMContentLoaded", function () {
    startPorcupine();
});
Sicherheitslücken wie Cross-Site-Scripting haben dafür gesorgt, dass moderne Browser Zugriffe auf File-URLs und andere Ressourcen blockieren. Jeder Webentwickler kennt mittlerweile den in den meisten Python-Distributionen enthaltenen Mini-Server, der sich aus der Kommandozeile heraus anwerfen lässt. Nachteilig ist bei ihm allerdings, dass er von Haus aus HTTPS nicht unterstützt - eine gravierende Schwäche, weil Teile der Picovoice-Engines intern mit HTTPS arbeiten.
Erfreulicherweise müssen Entwickler, die mit Node.js arbeiten, diese Krücke nicht verwenden. Mit dem als Teil von YARN entwickelten Serve-Paket steht ein Web-Server zur Verfügung, der sich komplett in den npm-Workflow einbindet. Unsere erste Aktion besteht deshalb darin, ihn durch einen Aufruf von npm auf die Workstations zu laden:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$
npm install serve
. . .
+ serve@11.3.2
Das Bereitstellen des Serve-Pakets reicht allerdings noch nicht aus. Als nächstes öffnen wir deshalb die Manifestdatei und ersetzen den von npm angelegten und mit einem leeren Test ausgestatteten Skript-Tag nach folgendem Schema und zur Aktivierung von Serve notwendigen Befehl:

"main": "index.js",
"scripts": {
    "start": "yarn run serve"
},
Nach der erfolgreichen Anpassung der Manifestdatei lässt sich die Applikation nach folgendem Schema im Serve-Webserver bereitstellen:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$
npm start
Drücken Sie hier Enter, sehen Sie ein Statusfenster, das sowohl eine lokale als auch eine im gesamten Netzwerk gültige URL anzeigt (Bild 2). Geben Sie diese danach in einem Browser ihrer Wahl ein, um die Applikation zu laden. Möchten Sie die Kontrolle über das Terminalfenster zurückerlangen, so drücken Sie stattdessen STRG plus C.
Der Server-Dienst stellt die Picovoice-Applikation bereit (Bild 2)
Quelle: Hanna
Im Rahmen des ersten Starts der Applikation sehen Sie - zumindest in den meisten Browsern - eine nach dem in Bild 3 gezeigten Schema aufgebaute Permission-Abfrage.
So erfolgt eine Permission-Abfrage im Browser (Bild 3)
Quelle: Hanna
Aus der Logik folgt, dass sie der Picovoice-Testwebseite den Zugriff auf die Mikrofon-Informationen gewähren müssen. Unsere Testumgebung hört nur auf das Wakeword Ananas. Wer es ins Mikrofon spricht, sieht die korrekte Aktivierung der dafür vorgesehenen Payload (Bild 4).
Picovoice Web hat das Wakeword Ananas richtig erkannt (Bild 4)
Quelle: Hanna
Jede Sprachversion von Picovoice wird mit guten zwei Dutzend Wakewords ausgeliefert, deren Nutzung im Rahmen einer Demonstrationsapplikation erfolgt. Möchte man Picovoice kommerziell und/oder mit eigenem Wakeword verwenden, so muss man die Engine lizenzieren.
Die eigentliche Bereitstellung der Wakeword-Dateien erfolgt dann in einem als Picovoice Console bezeichneten Dienst, der unter der URL https://console.picovoice.ai/ bereitsteht. Für umfangreichere Experimente empfiehlt sich die Verwendung der Enterprise-Variante der Konsole, weil die nur für nichtkommerzielle Anwender vorgesehene Computer-Variante einige Funktionen nicht zur Verfügung stellt. Erfreulicherweise gewährt Picovoice jedem, der dem Unternehmen seine E-Mail-Adresse gibt, eine 30 Tage dauernde Testphase.
Das Leben eines neuen Wakewords beginnt in diesem Fenster (Bild 5)
Quelle: Hanna
Nach der Anmeldung im Webdienst können Sie sich einerseits für die Wakeword-Engine Porcupine und andererseits für die in diesem Artikel noch zu besprechende Rhino-Intenterkennungsengine entscheiden. Da wir in den folgenden Schritten nur ein Wakeword trainieren wollen, ist Porcupine die korrekte Auswahl. Im Wakeword-Generierungsassistenten finden Sie danach einen Assistenten zum Anlegen eines neuen Wakeword (Bild 5). Wichtig ist, dass die durch die Combobox Language vorgenommene Sprachauswahl nur für das Inferenzmodell von Relevanz ist.
Die korrekte Plattformauswahl ist sehr wichtig (Bild 6)
Quelle: Hanna
Nach dem Anklicken der Option Wakeword erscheint ein Plattform-Auswahlfenster am Bildschirm, in dem sie die korrekte Zielplattform auswählen können (Bild 6). Beachten Sie, dass die Koeffizienten-Dateien, die die Picovoice-Konsole generiert, plattformspezifisch sind. Wer dasselbe Wakeword sowohl unter Android als auch im Web verwenden möchte, muss zwei separate Koeffizienten-Files erzeugen.
Nach dem Abnicken der Lizenzbedingungen können Sie auf den Train-Knopf klicken, um die Verifikation des Modells anzustoßen. Der Server beginnt die Verifikation, die in der Regel nur einige Sekunden dauert. Ist diese erfolgreich, scheint das Wakeword sofort in der Liste Custom Wakewords auf. Beachten Sie, dass die Erzeugung der eigentlichen Koeffizientendatei ein vergleichsweise Rechenleistungs-intensiver Prozess ist, der schon einmal drei Stunden Zeit in Anspruch nehmen kann.
Nach dem erfolgreichen Durchlaufen des Trainingprozesses sendet die Picovoice-Konsole jedenfalls eine E-Mail, die über die Bereitschaft der Datei informiert. In der Testumgebung hört die Datei auf den Namen hallo_leute_wasm_5_22_2021_v1.9.0.zip. Da der Dateiname sowohl das Datum als auch das Wakeword enthält, wird die Ihnen angebotene Datei wahrscheinlich einen anderen Namen haben. Beachten Sie zudem, dass die von einer Testversion der Picovoice-Konsole generierten Arbeitsdateien prinzipiell und immer nur 30 Tage lang gültig sind.
Im nächsten Schritt müssen wir das Archiv in eine bequem zugängliche Position im Dateisystem der Workstations extrahieren. Von besonderer Bedeutung ist dabei die Datei hallo_leute_wasm_2021-05-22-utc_v1_9_0.ppn, die die eigentlichen Modellinformationen für die Wakeword-Engine enthält.

Unterschiede der Versionen

An dieser Stelle findet sich ein erster gravierender Unterschied zwischen der im Browser lebenden Version und der Node-Variante des Produkts. Während die Node-Edition von Picovoice dank den umfangreichen Dateizugriffs-Funktionen direkt Pfade zu den .PPM-Dateien entgegennimmt, ist für das Provisioning gegen eine Webseite mehr Arbeit erforderlich.
Als Mittel der Wahl hat sich dabei das Base64-Format etabliert - eine Enkodierungsmethode, die Binärdateien in ein textuell übertragbares Format bringt. Auch an dieser Stelle zeigt sich ein weiteres Mal, warum Linux bei den meisten Software-Entwicklern populärer ist als Windows. Mit base64 steht ein auf Kommandozeilen-Ebene verfügbares Werkzeug zur Verfügung, das nach folgendem Schema direkt zur Base64-Enkodierung beliebiger Dateien herangezogen werden kann:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$ base64 hallo_leute_wasm_2021-05-22-utc_v1_9_0.ppn
24RTd6oGmcf9JjA2S4No5XEXRSOA7A. . .
Als problematisch erweist sich für diese Vorgehensweise, dass das Werkzeug von Haus aus davon ausgeht, dass die generierten Dateien in einer E-Mail verwendet werden sollen. Aus diesem Grund fügt es Return-Zeichen in den Datenstrom ein. Erfreulicherweise lässt sich dieses Problem durch Übergabe des Parameters -w 0 umgehen.
Da das manuelle Einsammeln von gut 3000 Zeichen aus der Kommandozeile beziehungsweise dem Terminalemulator nicht wirklich bequem ist, empfiehlt sich die Nutzung der Pipe-Funktionalität unter Linux. Dahinter steht der Gedanke, dass die von Kommandozeilen-Programmen generierte Ausgabe - wie hier durch das Präfix > demonstriert - in Dateien umgeleitet werden kann. Nach getaner Arbeit finden Sie im Arbeitsverzeichnis eine neue Datei, die eine einzige Zeile mit der gesamten Base64-Repräsentation der Wakeword-Informationen anliefert. Im nächsten Schritt kehren wir in die HTML-Datei zurück und adaptieren die Deklaration des Porcupine-Workers nach folgendem Schema:

let ppnEn = await PorcupineWebDeWorker.
PorcupineWorkerFactory.create({
  base64: "24R . . . rg==", custom: "tamskey",
  sensitivity: 0.7, });
Anstatt wie bisher das Attribut builtin zu übergeben, übergeben wir nun das Parameterpaar Base64 und Custom. In Base64 kommt dabei der von der soeben besprochenen Kommando-Sequenz generierte String unter, während Custom einen String übernimmt, mit dem die Wakeword-Engine die Erkennung unseres hauseigenen Strings später bei den Eventhandlern anzeigt.
Beklagt sich Porcupine über ungültige Eingabedaten, so liegt eventuell ein Bug vor (Bild 7)
Quelle: Hanna
Speichern Sie die Datei und führen Sie sie danach in einem Browser Wahl aus - idealerweise funktioniert die Erkennung des neuen Wakewords problemlos. In der Praxis kommt es stattdessen immer wieder einer Fehelermeldung (Bild 7).

Richtige Sprachvariante einstellen

Sollten Sie auf Ihrer Workstation diese Fehler ebenfalls zu Gesicht bekommen, so überprüfen Sie im ersten Schritt im Picovoice-Backend, ob sie die passende Zielplattform und die zur verwendeten Worker-Klasse passende Sprachvariante eingestellt haben. Ist dies der Fall, so müssen Sie die verwendeten Bibliotheken aktualisieren. Öffnen Sie dazu die Manifestdatei, und adaptieren Sie den dependencies-Block nach folgendem Schema:

"dependencies": {
  "@picovoice/porcupine-web-de-worker": "^1.9.4",
  "@picovoice/porcupine-web-en-worker": "^1.9.4",
  "@picovoice/web-voice-processor": "^2.0.2",
  "serve": "^11.3.2" }
Änderungen in der Projekt-Konfigurationsdatei setzen sich nicht automatisch in den aktuellen Paketspeicher des Node.js-Projekts um. Aus diesem Grund müssen wir auf der Kommandozeile nach folgendem Schema abermals eine Aktualisierung befehligen, um die diversen Porcupine-Komponenten auf den aktuellsten Stand zu bringen:

tamhan@TAMHAN18:~/picowebspace/porcupinewebdemo$ npm
install
. . .
updated 2 packages and audited 83 packages in 7.272s
Nach einem abermaligen Start des Webservers und einem Test des Programms, reagiert es auf das hauseigene Wakeword (Bild 8).
Das in der Konsole angelegte Wakeword führt zu einer Reaktion des Programms (Bild 8)
Quelle: Hanna
Die Wakeword-Erkennung funktioniert perfekt, solange wir es nur mit einfachen Kommandos zu tun haben. In der Praxis gibt es immer wieder Situationen, die zwar noch keine komplett freie Spracherkennungsart verlangen, aber andererseits von einer gewissen Flexibilität im Bereich der angelieferten Informationen profitieren.

Rhino erkennt Intents

Picovoice deckt dieses Bedürfnis über eine als Rhino bezeichnete Engine ab, die zwar ebenfalls grammatikbasiert ist, aber statt einfachen Wakewords vollwertige und analog zu Android als Intent bezeichnete Kommandos verarbeitet. Auch zur Vorführung der Rhino-Engine wollen wir auf eine bereitgestellte Engine setzen, die im Fall der Intent-Erkennung eine Art Standuhr realisiert. Analog zum ersten Beispiel benötigen wir auch diesmal ein Projektskelett, das sich auf der Kommandozeile durch folgende Befehle realisieren lässt:

tamhan@TAMHAN18:~/picowebspace$ mkdir rhinodemo
tamhan@TAMHAN18:~/picowebspace$ cd rhinodemo/
tamhan@TAMHAN18:~/picowebspace/rhinodemo$ npm init
Auch für die Arbeit mit für die Intentverarbeitung geeigneten Version der Engine müssen wir im ersten Schritt einige zusätzliche Pakete aus den verfügbaren Paketquellen herunterladen:

tamhan@TAMHAN18:~/picowebspace/rhinodemo$ npm install @picovoice/rhino-web-en-worker @picovoice/
web-voice-processor
. . .
+ @picovoice/web-voice-processor@2.0.2
+ @picovoice/rhino-web-en-worker@1.6.1
Auch hier gilt, dass die eigentliche Bereitstellung der Textdateien über einen Webserver zu erfolgen hat. Deployen Sie deshalb das von weiter oben bekannte Yarn-Serve-Paket abermals nach folgendem Schema. Die ebenfalls notwendige Integration des Servers in die Manifestdatei drucken wir an dieser Stelle aus Platzgründen nicht nochmals ab:

tamhan@TAMHAN18:~/picowebspace/rhinodemo$ npm install serve
. . .

+ serve@11.3.2
Wir wollen in den folgenden Schritten mit der englischen Version der Rhino-Engine arbeiten, weshalb wir die als Einsprungpunkt dienende Datei im Header mit der Inklusion der folgenden beiden Module beauftragen:

<!DOCTYPE html>
<html lang="en">
<head>
<script src="node_modules/@picovoice/rhino-web-en-
worker/dist/iife/index.js">
</script>
<script src="node_modules/@picovoice/web-voice-
processor/dist/iife/index.js">
</script>
Während die Porcupine-Engine von Haus aus mit einem kleinen Indifferenz-Sample ausgeschickt ausgeliefert wird, ist der Rhino-Anwender komplett auf sich selbst gestellt. Er muss eine als Kontext bezeichnete Grammatikdatei mitbringen, deren Aufbau in Bild 9 schematisch gezeigt ist.
Eine Rhino-Grammatik besteht aus mehreren Elementen (Bild 9)
Quelle: Hanna
Auf der obersten Hierarchieebene finden sich dabei die als Intents bezeichneten Materelemente. Sie stehen für Aktionen, die der Benutzer später durch Sprachbefehle auslösen kann. Jeder Interessent kann dabei ein oder mehrere Utterances mitbringen. Dabei handelt es sich um Varianten konkreter Formulierungen, die von der Engine aber zur Auslösung des selben Events eingesetzt werden sollen.
Zu guter Letzt gibt es Slots für häufig benötigte Informationstypen. Ein Klassiker dafür wäre das Entgegennehmen von Zahlen - eine Aufgabe, die so gut wie jeder Entwickler einer Sprachsteuerung irgendwann zu realisieren sucht. Um das Training dieser (durchaus anspruchsvollen) Aufgabe zu zentralisieren, wird von PicoVoice ein Slot angeboten, der sich um die Verarbeitung von Zahlen kümmert.
Für die eigentliche Bereitstellung der Test-Modellinformationen an den WebWorker kommt dann - analog zu Porcupine - abermals ein Base64-String zum Einsatz. Im Fall unseres Uhren-Demonstrationsbeispiels sieht der dafür notwendige Code, der im Body platziert wird, folgendermaßen aus:
<script type="application/javascript">
const CLOCK_CONTEXT_64 = "cmhpbm8xLjYuMNEaA . . .
Im Fall unserer Uhren-Demo müssen wir uns erfreulicherweise nicht mit der Bereitstellung der Modelldaten herumschlagen, sondern können einfach die URL https://github.com/Picovoice/rhino/blob/master/demo/web/index.html besuchen. Dort findet sich der fertige Kontext-String, den sie einfach via Zwischenablageübernehmen.
Die Verarbeitung von intentbasierten Sprachbefehlen ist insofern komplizierter, als die Engine dabei mehr Informationen ausgibt. Wir müssen die bekannte WriteMessage-Funktion anpassen, dass sie die angelieferten Informationen fortan in einem dedizierten Teil von index.htm unterbringt:

function writeMessage(message) {
  console.log(message);
  let p = document.createElement("p");
  let text = document.createTextNode(message);
  p.appendChild(text);
  document.getElementById("messages").
  appendChild(p);
}
Nebeneffekt der von writeMessage angeforderten zusätzlichen Nodes im DOM ist, dass der Rest des body-Tags unseres Test-Korpus nun folgendermaßen aussieht:

<button id="push-to-talk">Push to Talk</button>
<div id="messages"></div>
<0x000A><hr />
<h2>Context info:</h2>
<pre id="rhn-context-yaml"></pre>
</body>
Neben diversen Feldern beziehungsweise Mater-Tags, die für die Ausgabe der Informationen verantwortlich sind, finden wir hier auch einen PTT-Knopf. Auf diese Art und Weise kann der Benutzer die Intents-Engine bei Bedarf einschalten, was das Erbeuten von Intents aus normaler Konversation erschwert beziehungsweise verhindert.

Asynchrone Arbeitsfunktion

Angemerkt sei, dass dieser Knopf in praktischen Systemen nur allzu gern durch eine Wakeword-Erkennung ersetzt wird. Es spricht nichts dagegen, die Porcupine-Engine zur Auslösung ebendieser PTT-Ereignisse einzuspannen. Im nächsten Schritt müssen wir abermals eine asynchrone Arbeitsfunktion erzeugen, die sich um die Armierung der Sprachdaten-Verarbeitungspipeline kümmert:

async function startRhino() {
window.rhinoClockWorker = await
RhinoWebEnWorker.RhinoWorkerFactory.create({
  context: {
    base64: CLOCK_CONTEXT_64,
    sensitivity: 0.5,
  },
start: false,
}
);
Analog zu Porcupine gilt auch im Fall von Rhino, dass jede Sprachversion der Engine mit ihrem eigenen Materklasse-Satz ausgeliefert wird. Da wir hier mit einem in englischer Sprache gehaltenen Modell arbeiten wollen, lautet die korrekte Konstruktor-Klasse RhinoWebEnWorker.RhinoWorkerFactory.create. Als Parameter erwartet Sie ein JSON-Objekt, das neben dem Base64-String mit dem eigentlichen Interferenzmodell abermals einen Sensitivity-Parameter entgegennimmt. Er legt fest, ob die Picovoice-Engine mehr Intents erkennen soll, oder - auf Kosten schlechterer Erkennungsperformance - mehr False Positives zu eliminieren hat. Ist die Instanz erfolgreich im window-Objekt untergekommen, so schreiben wir ihr im nächsten Schritt ein Kommando ein:

rhinoClockWorker.postMessage({ command: "info" });
Wie im Fall von Porcupine gilt auch bei Rhino, dass die Verarbeitung der Ereignisse asynchron erfolgt. Unsere nächste Aufgabe ist deshalb das Einschreiben eines Eventhandlers, die bei der erfolgreichen Erkennung eines Intents aufgerufen wird. Wie immer gilt auch hier, dass wir im ersten Schritt anhand der in msg.data.command = angelieferten Event-Typ-Informationen makeln, um im Fall von erfolgreich empfangenen Nachrichten nach dem Schema JSON.stringify(msg.data.inference) die Message auszugeben:

window.rhinoClockWorker.onmessage = (msg) => {
  if (msg.data.command === "rhn-inference") {
    writeMessage("Inference detected: " +
    JSON.stringify(msg.data.inference));
    window.rhinoClockWorker.postMessage
    ({ command: "pause" });
    document.getElementById("push-to-talk").disabled =
    false;
Interessant ist in diesem Zusammenhang noch, dass wir nach dem erfolgreichen Abarbeiten eines Inferenz-Kommandos dafür sorgen müssen, dass die Engine wieder in den Ruhezustand wechselt. Dies erreichen wir ebenfalls durch Nutzung der Methode PostMessage, die nun aber ein Pause-Kommando als zu verarbeitenden String übergeben bekommt. Der Rest dieses Teils der Payload kümmert sich dann um DOM-Manipulation beziehungsweise darum, dass der PTT-Knopf wieder aktiviert wird.
Picovoice-Grammatiken fallen nicht vom Himmel (Bild 10)
Quelle: Hanna
Bei Experimenten mit Rhino waren wir bisher davon ausgegangen, dass das bereitgestellte Eieruhr-Inferenzmodell vom Himmel fällt. Dies ist nicht der Fall - im Hintergrund steht eine Grammatikdatei, die nach dem in Bild 10 gezeigten Prozess für die Verarbeitung durch die verschiedenen plattformspezifischen Engines aufbereitet wird. Wir werden uns die genaue Struktur der Grammatikdatei bald zu Gemüte führen, vorher sei auf eine kleine Komfort-Funktion hingewiesen. Die Web-Version der Picovoice-Engine ist in der Lage, die für die Erzeugung der Grammatik-Datei verwendete .yaml-Datei zur Laufzeit aus dem Base64-String zu rekonstruieren. Dieser an Reflektion erinnernde Prozess lässt sich dadurch auslösen, dass man an PostMessage ein info-Objekt zur Bearbeitung übergibt:

} else if (msg.data.command === "rhn-info") {
    writeMessage("Context info YAML received.")
    document.getElementById("rhn-context-yaml").
    innerText = msg.data.info;
    }
};
Im Fall des Eingehens von context-Informationen schreibt unsere Applikation diese jedenfalls nach außen.

Kommunikations-Toolchain

Im nächsten Schritt müssen wir uns darum kümmern, die eigentliche Kommunikations-Toolchain einzurichten. Analog zu Porcupine erfolgt auch hier die Beschaffung der Mikrofon-Zugriffsberechtigungen und anderer Nettigkeiten:

try {
  let webVp = await WebVoiceProcessor.WebVoiceProcessor.
  init({ engines: [window.rhinoClockWorker], });
  writeMessage( "WebVoiceProcessor ready! Press the
  'Push to Talk' button to talk. Then say e.g. 'Set a
  timer for one minute'" );
  } catch (e) {
  writeMessage("WebVoiceProcessor failed to
  initialize: " + e);
  }
}
Zur eigentlichen Fertigstellung des Programms müssen wir dann nach folgendem Schema im Rahmen der DOM-Initialisierung einen Knopf-Eventhandler einschreiben:

document.addEventListener("DOMContentLoaded", function () {
    startRhino();
    document.getElementById("push-to-talk").onclick =
    function (event) {
        writeMessage("Rhino is listening for your
        commands ...");
        this.disabled = true;
        window.rhinoClockWorker.postMessage
        ({ command: "resume" });
    };
});
An dieser Stelle ist auch diese Version des Programms zur Ausführung bereit - laden Sie sie über serve in einem Browser ihrer Wahl, und klicken Sie auf den PTT-Knopf, um mit dem System zu interagieren.

Grammatik im Detail

Nach einem erfolgreichen Start unseres Rhino-Programmbeispiels präsentiert es die in Bild 11 gezeigte Übersichtsseite. Besonders wichtig ist für uns in den folgenden Schritten die am Bildschirm eingeblendete Context-Info, die Informationen über die verwendete Grammatik bereitstellt.
Das Rhino-Programmbeispiel zeigt sich durchaus kommunikativ (Bild 11)
Quelle: Hanna
Als ersten Kandidaten wollen wir uns die Expressions-Struktur anschauen, die nach folgendem Schema aufgebaut ist:

context:
expressions: . . .
availableCommands: - what can I say
Rhino wird beim Aktivieren des jeweiligen Slots dem Programm die definierte Konstante zurückgeben. In natürlicher Sprache kann der User dies dadurch auslösen, dass er der Engine den String what can I say zur Verfügung stellt. Diese Theorie lässt sich in der Praxis bestätigen: Starten Sie das Programm und tätigen Sie die Spracheingabe, um sich an der folgenden Ausgabe zu erfreuen:

Rhino is listening for your commands ...
Inference detected: {"isFinalized":true,"isUnderstood":
true,"intent":"availableCommands","slots":{}}
Rhino is paused. Press the 'Push to Talk' button to speak again.
Die praktische Erfahrung lehrt, dass Benutzer normalerweise einige Grammatik-Varianten zum Ausdrücken einer bestimmten Intention verwenden. Das vorliegende Beispiel demonstriert dies durch den Intents setTimer, der auf verschiedene Arten aufzurufen ist:

context:
  expressions:
    setTimer:
      - set (a) timer for $pv.TwoDigitInteger:hours
        [hour, hours]
      - set (a) timer for $pv.TwoDigitInteger:minutes
        [minute, minutes]
      - set (a) timer for $pv.TwoDigitInteger:seconds
        [second, seconds]
      - . . .
      - set (a) timer for $pv.TwoDigitInteger:hours
        [hour, hours] (and)
        $pv.TwoDigitInteger:seconds [second, seconds]
Die nach dem Schema set (a) timer in Klammern gesetzten Ausdrücke sind dabei sogenannte Optionalen. Darunter versteht man in der Welt von Picovoice Teile der Grammatik, die zur Aktivierung des Slots nicht unbedingt erforderlich sind.
Die nach dem Schema $pv.TwoDigitInteger:hours aufgebauten Ausdrücke deklarieren Slots. Hier haben wir es vor allem mit Zeitinformationen zu tun, weshalb der für die Entgegennahme von zweistelligen Zahlen verantwortliche Slot pv.TwoDigitInteger zum Einsatz kommt. Von Haus aus bringt Picovoice übrigens sieben Slots mit, die häufig benötigte Daten entgegennehmen: pv.Alphabetic, pv.Alphanumeric, pv.Percent, pv.SingleDigitInteger, pv.SingleDigitOrdinal, pv.TwoDigitInteger und pv.TwoDigitOrdinal. Beachten Sie, dass der Entwickler einer Picovoice-Erkennungsapplikation nicht auf die implementierten Slots beschränkt ist. Es ist auch erlaubt, einen eigenen Slot festzulegen:

setAlarm:
  . . .
  - set (an) alarm for $day:day (at)
    $pv.TwoDigitInteger:hour
Die eigentliche Deklaration des Slots findet sich dann eine Spalte weiter unten und ist nach folgendem Schema aufgebaut:
slots:
  . . .
  day:
    - today
    - tomorrow
    - monday
    - tuesday
    - wednesday
    - thursday
    - friday
    - saturday
    - sunday
Lohn der hier abgedruckten Slot-Deklaration ist, dass der Benutzer setAlarm fortan mit verschiedenen Parametern aufrufen kann.

Picovoice auf Abwegen

Die Ausführung von Picovoice-Interferenzmodellen über einen Browser ist nur eine der Möglichkeiten der WebAssembler-Variante der Engine. Wer die Produkte Angular, React oder Vue verwendet, bekommt direkt integrierbare Versionen angeboten, die Besonderheiten der jeweiligen JavaScript-Frameworks nativ unterstützen. Nutzen Sie Javascript stattdessen auf dem Server, so können Sie auch Node.js beauftragen. Für die Arbeit mit Porcupine müssen sie in Node.js im ersten Schritt das für diese Ausführungsumgebung vorgesehene Paket herunterladen und installieren:

npm install @picovoice/porcupine-node
Im nächsten Schritt erfolgt die Einrichtung der Erkennungspipeline nach folgendem Schema:

const Porcupine = require("@picovoice/porcupine-node");
const {
  GRASSHOPPER, BUMBLEBEE, } = require
  ("@picovoice/porcupine-node/builtin_keywords");
  let handle = new Porcupine([GRASSHOPPER, BUMBLEBEE],
  [0.5, 0.65]);
Die Node.js Version von Picovoice unterscheidet sich dann noch insofern von ihrem clientseitigen Kollegen, als der Betrieb der Arbeitsschleife in der Verantwortung des Entwicklers liegt.

Fazit

Picovoice mag vom Funktionsumfang her nicht mit Alexa und Co. mithalten können. Allerdings lebt das System komplett lokal und kann direkt lizenziert werden. Die sind durchaus Vorteile. Zudem ist die Erkennungsgenauigkeit dank der Verwendung fixer Grammatiken auch bei schlechteren Mikrofonen hervorragend.




Das könnte Sie auch interessieren