Alexa selbstgebaut -Bing Speech API

Spracheingabe ist der neueste Trend. Dabei geht es nicht nur um das erkennen eines Wortes oder Befehl, es geht um Sinn, Kontext, Stimmung, Übersetzung und sogar um Authentifizierung. Also weit über Diktierfunktion hinaus. Windows enthält schon sehr lange eine recht ausgefeilte API um Sprache zu erkennen. Das bedeutet es ist mit .net oder UWP dank dem Namensraum System.Speech oder Windows.Media.SpeechRecognition  offline möglich mit Sprache zu agieren.

Richtig nett wird Interaktion mit Sprache allerdings erst wenn AI zum Einsatz kommt. Die Produkt Familie von Microsoft nennt sich dazu Cognitive Services. Konkret die Bing Speech APi. Mit 20 gratis Anforderungen pro Minute für jedermann mit einer Microsoft ID nutzbar.

image

Man erhält einen API Key in der Form 1b877c4bbba24b26acb7019afbdb95af  und eine als Endpoint bezeichnet Url

https://api.cognitive.microsoft.com/sts/v1.0

 

Als Vorbereitung für die Aufzeichnung der Sprache empfehle ich meinen Blog Artikel. Es wird zwar die Audio Aufzeichnung als Wav File gespeichert, man kann aber in der Praxis auch über den Stream direkt gehen.

Als nächstes müssen wir vier Probleme lösen. Für die Nutzung des Bing Services muss ein Authentifizierungs Token erstellt werden. Dann muss das Wav File in Blöcken eingelesen werden. Diese Blöcke werden an den Bind Service per Chunk Post übermittelt. Das Ergebnis wird ausgelesen und in eine Objekt Struktur deserialisiert. Eine Menge zu tun. Legen wir los.

 

Um das Thema zu isolieren bekommt die UI im XAML einen Button und eine Textbox. Dem asynchronen Button Event fügt man den Code hinzu um den Bearer Token zu beziehen.

   1:  Using client = New HttpClient()
   2:      client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", "ersetzen durch API Key")
   3:      Dim UriBuilder = New UriBuilder("https://api.cognitive.microsoft.com/sts/v1.0")
   4:      UriBuilder.Path += "/issueToken"
   5:      Dim result = Await client.PostAsync(UriBuilder.Uri.AbsoluteUri, Nothing)
   6:      text1.Text = Await result.Content.ReadAsStringAsync()
   7:  ...

 

Dieser soll 10 Minuten gelten. Es ist also überflüssig, bei jeder Spracherkennung diesen erst anzufordern. Um das Beispiel klein zu halten, mach ich es trotzdem so. In meiner Textbox lässt sich so gut ablesen ob das funktioniert.

Nun muss das Wav File gelesen und in 1KB große Blöcke zerlegt werden. Die API fordert das so. Streams bereiten mir in UWP wenig Freude weil anders implementiert wie in System.IO. Meine optimale Lösung unter Berücksichtigung der beschrnänkten Dateizugriffsrechte von UWP nutzt den Binary Reader und konvertiert den Stream in ein Byte Array. Für die Berechnung der Anzahl der Reads reicht die Länge des BaseStreams.

   1:  Dim f = Await KnownFolders.MusicLibrary.GetFileAsync("audio.wav")
   2:  Using fs = Await f.OpenSequentialReadAsync()
   3:       Dim buffer(1024) As Byte
   4:       Using sr = New BinaryReader(fs.AsStreamForRead())
   5:           Dim lang = sr.BaseStream.Length
   6:           While lang > 0
   7:               buffer = sr.ReadBytes(1024)
   8:               ...todo block send
   9:               lang -= 1024
  10:          End While
  11:      End Using
  12: End Using

Diese 12 Zeilen Code haben mich 2 h gekostet, weil ich mehrere Varianten probiert habe.

 

Schritt Drei sendet die Byte Chunks der Audio Datei per HTTP Post Kommando an die Bing API. Dazu wird das HTTPWebRequest Objekt mit den nötigen Parametern gefüttert. Unter anderem den Token, der im ersten Code Block gewonnen wurde und nun im Textblock wartet. Der Vorige VB.NET Code wird nun um ein weiteres Using ummantelt, das eine Instanz des  Request als Stream erzeugt. In diesem Stream, werden dann die 1024 Byte Binär Blöcke an den Service gesendet (Zeile 14).

 

   1:   Dim request As HttpWebRequest = HttpWebRequest.Create(
"https://speech.platform.bing.com/speech/recognition/interactive/cognitiveservices/v1?language=de-de&format=detailed")
   2:  request.Accept = "application/json;text/xml"
   3:  request.Method = "POST"
   4:  request.ContentType = "audio/wav; codec=audio/pcm; samplerate=16000"
   5:  request.Headers("Authorization") = "Bearer " + text1.Text
   6:  Using requestStream = Await request.GetRequestStreamAsync()
   7:       Dim f = Await KnownFolders.MusicLibrary.GetFileAsync("audio.wav")
   8:       Using fs = Await f.OpenSequentialReadAsync()
   9:            Dim buffer(1024) As Byte
  10:                 Using sr = New BinaryReader(fs.AsStreamForRead())
  11:                     Dim lang = sr.BaseStream.Length
  12:                          While lang > 0
  13:                              buffer = sr.ReadBytes(1024)
  14:                              requestStream.Write(buffer, 0, buffer.Length)
  15:                              lang -= 1024
  16:                          End While
  17:                   End Using
  18:             End Using
  19:         requestStream.Flush()
  20:  End Using

 

Hier telefoniert also die UWP App erfolgreich nach Hause. Allerdings hören bzw lesen wir noch keine Übersetzung. Erst wird auch ein Response Objekt benötigt. Kurze Zeit nach dem Flush antwortet der Service und man erhält die Antwort ebenfalls als Stream.

   1:  Using response = Await request.GetResponseAsync
   2:       Using sr = New StreamReader(response.GetResponseStream())
   3:             text1.Text = sr.ReadToEnd()
   4:       End Using
   5:  End Using

 

Das Ergebnis liest sich nicht so toll. Am besten kopiert man den Inhalt der Textbox in die Zwischenablage. In meinem Beispiel kommt folgende JSON Struktur als Antwort.

{"RecognitionStatus":"Success","Offset":3200000,"Duration":16500000,"NBest":[{"Confidence":0.7879451,"Lexical":"ist alina zu jung","ITN":"ist Alina zu jung","MaskedITN":"ist Alina zu jung","Display":"Ist Alina zu jung."},{"Confidence":0.7879451,"Lexical":"wie ist alina zu jung","ITN":"Wie ist Alina zu jung","MaskedITN":"Wie ist Alina zu jung","Display":"Wie ist Alina zu jung?"},{"Confidence":0.7835977,"Lexical":"wer ist alina zu jung","ITN":"Wer ist Alina zu jung","MaskedITN":"Wer ist Alina zu jung","Display":"Wer ist Alina zu jung?"},{"Confidence":0.5198187,"Lexical":"ist alina ziehung","ITN":"ist Alina Ziehung","MaskedITN":"ist Alina Ziehung","Display":"Ist Alina Ziehung."},{"Confidence":0.5198187,"Lexical":"wie ist alina ziehung","ITN":"Wie ist Alina Ziehung","MaskedITN":"Wie ist Alina Ziehung","Display":"Wie ist Alina Ziehung?"}]}

Mein Tipp zum Schluss. Die Visual Studio Funktion Paste Json as Class nutzen und eine Objekt Klasse erzeugen lassen.

image 

Die generierte Klasse hat eine Schwäche mit Eigenschaften als Arrays, die erst bei deserialisieren auftritt. Deswegen füge ich zwei Änderungen ein- hier gelb markiert.

   1:  Public Class RestText
   2:      Public Property RecognitionStatus As String
   3:      Public Property Offset As Integer
   4:      Public Property Duration As Integer
   5:      Public Property Nbest As List(Of Nbest1)
   6:  End Class
   7:   
   8:  Public Class Nbest1
   9:      Public Property Confidence As Single
  10:      Public Property Lexical As String
  11:      Public Property ITN As String
  12:      Public Property MaskedITN As String
  13:      Public Property Display As String
  14:  End Class

 

Nun noch JSON.NET per Nuget Manager in Visual Studio dem Projekt hinzufügen und mit folgender Zeile das Objekt füllen

 

   1:  Dim daten = New RestText
   2:  daten = JsonConvert.DeserializeObject(Of RestText)(text1.Text)

Nun braucht man kein Alexa, Siri und Cortana mehr. Hey ppedv!

Kommentare sind geschlossen