Nachdem ich dann genug mit Sprachsynthese herumgeblödelt hatte, wollte ich noch ein Stück in die entgegengesetzte Richtung gehen und Spracherkennung ausprobieren. Es sollte hier nicht um das transkribieren von Sätzen gehen, sondern eher um die Erkennung einiger definierter Kommandos. Wieder soll das ganze rein lokal laufen und dabei halbwegs resourcensparend sein, und hier kommt Sphinx ins Spiel. Ein kleines Tutorial.

CMU Sphinx ist ein Spracherkennungsprojekt der Carnegie Mellon University in Pittsburgh, Pennsylvania. Das Projekt existiert seit über 20 Jahren und wird immer noch weiterentwickelt. Version 5.0.0 von Sphinx4 erschien am 13. Oktober 2022.

Die Sphinx lauscht

Das kleine Java-Programm soll auf das Mikrofon hören und die Sprache des Nutzers in Echtzeit in der Konsole transkribieren.

build.gradle

Für diese kleine Demo greife ich auf Version 5prealpha-SNAPSHOT zurück, die auf sonatype.org gehostet wird.

repositories {
    mavenLocal()
    mavenCentral()
    maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
}

dependencies {
    implementation group: 'edu.cmu.sphinx', name: 'sphinx4-core', version: '5prealpha-SNAPSHOT'
    implementation group: 'edu.cmu.sphinx', name: 'sphinx4-data', version: '5prealpha-SNAPSHOT'
}

SpeechRecognizer.java

Der SpeechRecognizer soll später in einem eigenen Thread laufen. Wir verwenden für die Demo eine englische Spracherkennung. Ich muss hier ein AcousticModel, eine LanguageModel und ein Dictionary definieren. Der LiveSpeechRecognizer lauscht auf das Mikrofon und transkribiert live.

import java.io.IOException;

import edu.cmu.sphinx.api.Configuration;
import edu.cmu.sphinx.api.LiveSpeechRecognizer;
import edu.cmu.sphinx.api.SpeechResult;

public class SpeechRecognizer implements Runnable {
	
	String path = "resource:/edu/cmu/sphinx/models/en-us/";
	LiveSpeechRecognizer recognizer;
	
	public SpeechRecognizer() throws IOException {
		
		final Configuration configuration = new Configuration();
		configuration.setAcousticModelPath(path + "en-us");
		configuration.setDictionaryPath(path + "cmudict-en-us.dict");
		configuration.setLanguageModelPath(path + "en-us.lm.bin");
		recognizer = new LiveSpeechRecognizer(configuration);
	}
	
	@Override
	public void run() {
		recognizer.startRecognition(true);
		SpeechResult result;
		while ((result = this.recognizer.getResult()) != null) {
			String hypothesis = result.getHypothesis();
			if (hypothesis != null) {
				System.out.println(hypothesis);
			}
		}
	}
}

Main.java

Die „public static void main“ ist mal wieder überschaubar:

import java.io.IOException;

public class Main {
	
	public static void main(String[] args) throws IOException {
		SpeechRecognizer speechRecognizer = new SpeechRecognizer();
		new Thread(speechRecognizer).start();
	}
}

Ein erster Test: Ernüchterung

Für einen ersten kleinen Test lese ich Sphinx einen Satz von https://www.esa.int/Science_Exploration/Space_Science/Webb vor:

„The James Webb Space Telescope is the next great space science observatory following Hubble.“

Meine Versuche resultieren in folgenden ernüchternden Interpretationen:

  • „chains wet spit the pope is the best with this i’m salsa people republic“
  • „the shameful say to school that gripped the size of the republic“
  • „and and wes a look when i eat besides we going on“

Um Einwänden zuvor zu kommen, dass meine englische Aussprache vielleicht „not ze best“ ist, sei erwähnt, dass der Google Assistant in einem kurzen Test mit meinem Smartphone den Text fehlerfrei transkribieren konnte. Neben den erschreckend nutzlosen Ergebnissen fühlt sich das Programm sehr träge an und der eingebaute Memory Tracker gibt an, etwa 2 GB zu brauchen.

Allerdings sollte das auch gar nicht der Anwendungsfall sein. Statt einer kompletten Transkription soll das Programm nur ein paar kurze Befehle interpretieren können. Also versuche ich es mit etwas einfacherem. Ich spreche: „Computer, please open the browser.“ Immerhin versteht Sphinx nun das Wort „Computer“ in etwa der Hälfte der Fälle (sonstige Ergebnisse: „ultra“, „contract“, „corbett“ und „couch“) aber aus „open the browser“ wird auch mal gerne „open a bottle“, „open the problem“ oder einfach „and girls up“. In diesem Zustand ist Sphinx also kaum zu gebrauchen.

Ein Hack

Die bisherigen Ergebnisse bestehen zum Großteil aus Worten, die ich nicht als Teil meiner Kommandos erwarten würde. Was wäre, wenn wir die Anzahl and Möglichkeiten einfach drastisch reduzieren?

Basierend auf der original cmu-en-us.dict Datei bastel ich mir eine reduzierte cmu-reduced-en-us.dict zusammen und passe den Pfad mit setDictionaryPath an. Das neue Dictionary enhält nur eine paar Wörter:

computer K AH M P Y UW T ER
open OW P AH N
close K L OW S
please P L IY Z
browser B R AW Z ER
editor EH D AH T ER
settings S EH T IH NG Z
the DH AH
player P L EY ER
console K AA N S OW L
chat CH AE T

Das Programm braucht nun leider sehr lange um zu starten: Jedes fehlende Wort wird als Warning geloggt. Dann aber überrascht Sphinx mit einer sehr flotten und nahezu fehlerfreien Spracherkennung – wenn man sich denn auf diese eingeschränkte Liste an Wörtern beschränkt. Auch die Speicherauslastung ist mit 200 MB nur noch ein Zehntel so groß.

Grammatik

Die tausenden Warnungen deuten schon an, dass das wohl nicht der vorgesehene Weg ist, Sphinx zu optimieren. Stattdessen sollte man eine Grammatik definieren. Dazu lege ich wieder eine Datei command-grammar.gram an:

#JSGF V1.0
grammar grammar;
public <basicCmd> = computer <command> (please)*;

<command> = <action> <object>;
<action> = /2/ open | /1/ close;
<object> = [the | a] (browser | settings | player | chat | console);

Diese wird dann im SpeechRecognizer konfiguriert:

configuration.setGrammarName("command-grammar");
configuration.setGrammarPath("resource:/");
configuration.setUseGrammar(true);

Und voilà, die Ergebnisse sind ähnlich gut wie im vorherigen Hack, aber das Programm startet jetzt ohne Warnungen. Außerdem lässt sich hier zum Beispiel definieren, dass alle Kommandos mit „Computer“ starten, dann die Aktion und schließlich das Objekt erwähnen müssen. Um die Features von JSGF (Java Speech Grammar Format) aufzuzeigen, wird soll in dem Beispiel davon ausgegangen sein, dass „open“ doppelt so wahrscheinlich auftritt wie „close“, und dass Höflichkeit gegenüber Computern optional ist (daher das Sternchen hinter „please“).

Und noch ein bisschen Blödsinn: Sphinx vs Mary

Zum Schluss noch ein kleines Experiment: Was wäre, wenn man Sphinx Sätze interpretieren lässt, die von MaryTTS eingesprochen würden? Das Spiel spielt man so lange, bis sich Eingabe und Ausgabe decken. Wie viele Iterationen braucht es wohl, bis sich Sphinx und Mary einig sind?

Selbstgespräch #1

Mit der Standardstimme „cmu-slt-hsmm“ von MaryTTS kommt sehr schnell nur noch Quatsch von Sphinx zurück. Auffällig ist auch, dass sich hier irgendwann eine Abfolge von Interpretationen zu wiederholen scheint: Nach dem zweiten „ah“, „prove it“, „war ah sh home“ habe ich dann abgebrochen.

> The James Webb Space Telescope is the next great space science observatory following Hubble.
> the chrome or flu sh
> hannah old who knew old from home if shaq
> home room
> take him long
> hi ah fang moon
> area who showroom all
> le old high sheen
> mm mm mm mm
> ah a high
> this one
> ash who
> huh how
> in
> home
> e.
> ah
> prove it
> war ah sh home
> moon old hatch ah one
> in ah hum
> ah for
> for lol
> louis room
> mm mm flash
> home home home hung from roof
> all for one higher for food wall
> proof old moon half old fool mm mm mm
> huh sure mm mm
> who come home
> ha more
> says who
> sherman shirt
> how whole womb flu
> mm mm mm mm mm mm mm hole
> ah
> prove it
> war ah sh home

Selbstgespräch #2

Im nächsten Versuch verwende ich die Stimme „Prudence“, die sich etwas natürlicher anhört. Und tatsächlich werden die Ergebnisse deutlich besser und die Schleife kommt nach nur sechs Durchläufen zum Ende:

> The James Webb Space Telescope is the next great space science observatory following Hubble
> the chains like space telescope is the next great space science observatory fawning harpo
> she leans likes a statistic is the next great space science observatory full mean how can
> he means like the statistic is the next great space science itself into a full mean how can
> he means like a statistic is the next great space science itself into a full mean how can
> he means like a statistic is the next great space science itself into a full mean how come

Trotzdem gut genug?

Sofern man die Grammatikfunktion verwendet ist Sphinx für meinen Anwendungsfall – dem Erkennen von ein paar definierten Kommandos – durchaus brauchbar. Will man mehr daraus machen, gibt es noch Möglichkeiten wie eine Acoustic Model Adaption, die vermutlich noch mehr Treffsicherheit herauskitzeln könnte.