Marno Janetzky

Geschätzte Lesezeit 11 Minuten

Techblog

OpenAI Whisper auf dem Raspberry Pi 4 – Live-Transkription

Wir bei MaibornWolff entwickeln nicht nur Software für smarte Geräte, wir beschäftigen uns auch mit neuen Technologien. Ich wollte wissen, ob eine Live-Transkription mit OpenAI Whisper auch auf einem kleinen Einplatinencomputer funktioniert und habe das Ganze getestet. In diesem Beitrag zeige ich, was das Sprachmodell bisher leisten kann – und wo seine Grenzen liegen.

Was ist OpenAI Whisper überhaupt?

Whisper ist ein KI-basiertes, automatisches Spracherkennungssystem von OpenAI und wurde mithilfe eines umfangreichen Audiodatensatzes aus dem Internet trainiert. Whisper ist nicht auf eine bestimmte Anwendung oder Sprache beschränkt, sondern universell einsetzbar. Ob es darum geht, gesprochene Worte in geschriebenen Text umzuwandeln, zwischen verschiedenen Sprachen zu übersetzen oder Sprecher anhand ihrer Stimme zu identifizieren, Whisper zeichnet sich durch seine Multitasking-Fähigkeiten aus.

Im Gegensatz zu den klassischen Modellen, die sich auf die einfache lineare Abfolge von Wörtern beschränken, hat Whisper eine beeindruckende Fähigkeit: Es kann komplexe Abhängigkeiten zwischen Wörtern und Sätzen erkennen. Wie schafft es das? Ganz einfach: Whisper basiert auf Transformatoren, einer innovativen Architektur für neuronale Netze. Diese Transformatoren ermöglichen es dem Modell, komplexe Beziehungen bzw. den Kontext zwischen den Sätzen zu erfassen.

Die Anwendungsmöglichkeiten von Whisper sind äußerst vielfältig. Überall dort, wo menschliche Sprache eine Rolle spielt, eröffnen sich Potenziale für den Einsatz von Whisper – sei es in sprachbasierten Assistenzsystemen oder in eingebetteten Geräten, die auf natürliche Weise durch Sprache gesteuert werden können. Ein weiterer großer Vorteil von Whisper besteht darin, dass die KI-Technologie in der Lage ist, mehrere Sprachen zu verstehen und darauf zu reagieren. Dadurch wird es möglich, Whisper in einer mehrsprachigen Umgebung einzusetzen, was besonders wichtig ist, wenn eine Anwendung weltweit genutzt werden soll.

Motivation für die Live-Transkription auf dem Raspberry Pi 4

Whisper wurde speziell für die Transkription von Audiodateien entwickelt. Auf einem leistungsfähigen Computer, wie einem modernen Laptop, kann eine solche Datei relativ schnell in Text umgewandelt werden. Bei einem leistungsschwächeren Computer wie dem Raspberry Pi kann dieser Prozess zwar länger dauern, das Ergebnis bleibt aber dasselbe: eine präzise Transkription.

Spricht man jedoch von Live-Transkription, so ist damit ein System gemeint, das in Echtzeit auf eine Spracheingabe reagiert und schnell ein Ergebnis liefert. Der Zeitfaktor spielt hier also eine weitaus größere Rolle als bei der Transkription einer einfachen Audiodatei.

Anstatt die Live-Transkription auf einem leistungsstarken Computer durchzuführen, habe ich mich bewusst dafür entschieden, das Ganze auf einem kleinen Einplatinencomputer mit begrenzter Rechenleistung auszutesten. Die Frage, ob dies gelingen kann, fand ich spannend und hat mich motiviert, den Versuch mit dem Raspberry Pi 4 zu starten. Eine erfolgreiche Umsetzung könnte aus Sicht unseres Teams die Basis für zahlreiche Anwendungen von Whisper auf Embedded Devices schaffen. Der Reiz lag für mich darin, die Grenzen der Technologie auf einem weniger leistungsfähigen, aber dennoch vielseitigen Gerät auszuloten. Man könnte sich jetzt an dieser Stelle noch fragen, wieso ich mich nicht gleich für den Raspberry Pi 5 entschieden hab, die Antwort ist einfach: Der war zu dieser Zeit einfach noch nicht verfügbar.

Meine verwendete Hardware

  • Raspberry Pi 4 Model B, 8GB RAM
  • Offizielles Raspberry Pi Netzteil
  • USB-Mikrofon

Der Weg zur Live-Transkription und ihre Funktionsweise

Nun, wie bin ich vorgegangen? Erstmal habe ich viel recherchiert und geschaut, was es in der Open-Source Community überhaupt schon alles gibt. Ich habe zahlreiche Projekte und sogar komplette Re-Implementierungen rund um Whisper gefunden, einige hatten sogar schon Ansätze zum Thema Live-Transkription. Und genau mit diesen habe ich dann nach dem Trial-and-Error Prinzip herumprobiert und sie ausgetestet. Da war vieles dabei, was entweder gar nicht oder nur sehr langsam funktionierte. Irgendwann bin ich dann auf das Projekt “Whisper-Ctranslate2” gestoßen, welches bereits eine – auf den ersten Blick – vielversprechende Live-Transkriptionsfunktionalität beinhaltet.

Whisper-Ctranslate2 ist ein Projekt, welches das Projekt “Faster-Whisper” in ein Kommandozeilenprogramm integriert. Faster-Whisper wiederum ist eine Re-Implementierung von OpenAIs Whisper und ist bei gleicher Genauigkeit bis zu viermal schneller und benötigt sogar weniger Speicher. Die Effizienz kann durch 8-Bit-Quantisierung sogar noch weiter gesteigert werden, sowohl auf der CPU (Central Processing Unit) als auch auf der GPU (Graphics Processing Unit). Daher schien es mir sinnvoll an dieser Stelle mit Faster-Whisper fortzufahren.

Jedoch funktionierte auch in Whisper-Ctranslate2 die Live-Transkription noch nicht wirklich zufriedenstellend. Ich vermutete aber aufgrund von Tests und Beobachtung des Verhaltens, dass es nicht an der Transkriptionsgeschwindigkeit lag. Also habe ich mir den Code genauer angesehen und überlegt, wie ich den zum Laufen bringen könnte.

Der Code für die Live-Transkription funktioniert im Prinzip wie folgt: Das Mikrofon wird kontinuierlich in Blöcken von 30ms ausgelesen. Die 30ms Datensätze werden dann periodisch an eine Funktion übergeben. Diese detektiert erkennt anhand eines Blocks, ob gerade gesprochen wird oder nicht. Dazu wird aus dem Block der “Root Mean Square”(RMS) berechnet und die Frequenz ausgewertet. Den RMS-Wert kann man sich als durchschnittliche Lautstärke vorstellen, der über einem vom Programm definierten Schwellenwert liegen muss. Die Frequenz muss in einem definierten Bereich liegen, der in etwa dem Frequenzband einer Stimme entspricht. Im Code sieht das dann so aus:


1 def _is_there_voice(self, indata, frames):
2         freq = np.argmax(np.abs(np.fft.rfft(indata[:, 0]))) * SampleRate / frames
3         volume = np.sqrt(np.mean(indata**2))
4
5         return volume > self.threshold and Vocals[0] <= freq <= Vocals[1]

Wenn diese beiden Bedingungen erfüllt sind, wird eine Stimme erkannt und das Programm beginnt mit der Aufnahme. Das Programm speichert nun blockweise in einem Zwischenspeicher. Nach wie vor wird aber bei jedem Block ausgewertet, ob die Person noch spricht. Wird nun bei einer vom Programm festgelegten Anzahl von hintereinander folgenden Blöcken, keine Stimme mehr erkannt, endet die Aufnahme.

Es müssen mehrere Blöcke sein, damit kurze Sprechpausen oder Luftholen nicht als Ende der Aufnahme gewertet werden. Eine beendete Aufnahme wird nun über eine Queue an einen weiteren Thread übergeben und parallel transkribiert. Währenddessen wertet der erste Thread weiterhin Blöcke aus und ist jederzeit bereit, eine neue Aufnahme zu starten. Die folgende Abbildung veranschaulicht diesen Ablauf (stark vereinfacht):

3 Hindernisse – 3 Lösungen

Nachdem ich das Prinzip hinter dem Code verstanden hatte, konnte ich drei Probleme identifizieren:

  1. Die Reaktionsfähigkeit der Live-Transkription war langsam
  2. Wenn Sätze schneller aufgenommen werden, als der Raspberry Pi 4 sie transkribieren kann, werden sie nicht in der korrekten Reihenfolge ausgegeben
  3. Bei der Live-Transkription wird häufig das erste Wort verschluckt

Das erste Problem war nur eine Konfiguration. Das Programm hat für meinen Geschmack zu lange nach dem Sprechen die Blöcke, in denen nicht gesprochen wird, abgewartet. Ich habe diese Zeit auf etwas unter 1s reduziert, wodurch sich die Live-Transkription reaktiver angefühlt hat.

Das zweite Problem war ein Datenstrukturproblem. Die Aufnahmen wurden einfach in einen Zwischenspeicher eingefügt und wieder entnommen (Stapelprinzip). Solange nur eine Aufnahme im Zwischenspeicher war, funktionierte dies. Wenn sich jedoch bereits eine Aufnahme im Zwischenspeicher befand und eine weitere hinzugefügt wurde, wurde zuerst diese neue Aufnahme transkribiert und nicht die die zuerst reinkam. Die Lösung hierzu war die Verwendung einer Queue, die nach dem FIFO-Prinzip arbeitet. Die Queue wird von vorne gefüllt und die Aufnahmen werden von hinten entnommen.

Die Lösung des dritten Problems stellte mich im Vergleich zu den anderen beiden vor die größte Herausforderung. Ich habe vermutet, dass die Spracherkennung etwas ungenau ist und die Stimme erst in der Mitte des ersten Wortes erkannt wird. Das Whisper-Modell bekommt dann nur ein halbgesprochenes Wort und weil es sich den Rest davon nicht zusammenreimen kann, schneidet es das Wort einfach ab. Soweit meine Vermutung.

Nun habe ich mich gefragt, wie ich das besser lösen kann und bin auf die Idee gekommen, auch ohne aktive Spracherkennung eine bestimmte Anzahl von Blöcken vorzuspeichern. Während der Aufnahme werden diese Blöcke in eine Fixed-Size-Queue geschrieben, d.h. es kann nur eine bestimmte Anzahl von Blöcken in der Queue sein. Konkret habe ich eine Double Ended Queue (Deque), eine doppelt verkettete Warteschlange, verwendet. Diese erlaubt das Hinzufügen und Entfernen von Elementen. Im Prinzip kann man sich die Deque wie eine Röhre vorstellen: Blöcke werden links in die Deque geschoben und wenn diese voll ist, fällt der älteste Block rechts heraus und wird gelöscht.

Wird nun eine Stimme erkannt, werden diese vorgespeicherten Blöcke einfach an den Anfang der Sprachsequenz gehängt. Nach kurzem Herumprobieren stellte sich heraus, dass 15 Blöcke bzw. 450ms den gewünschten Effekt erzielen. Ich hatte also Recht mit meiner Vermutung und damit war mein drittes Problem gelöst: Die Wörter wurden endlich vollständig erkannt und nicht mehr bei der Live-Transkription verschluckt.

Im Code sieht das Ganze nun wie folgt aus. Die Darstellung hier ist nur ein kleiner Ausschnitt aus dem Python-Code, um meine Änderung mit der “PreBufferedBlockSize” zu zeigen.

1 PreBufferedBlockSize = 15 # Number of blocks to be buffered continuously. Example 15 * BlockSize (30) = 450ms
2 EndBlocks = 30  # Number of blocks to wait before sending (30 ms is block)
3 self.preBufferedBlocks = deque(maxlen=PreBufferedBlockSize)
4 self.speaking = False
5 # This "callback" function is called by the function that reads the 30ms blocks from the microphone. Indata is one block.
6 def callback(self, indata, frames, _time, status):
7   voice = self._is_there_voice(indata, frames)
8   if not voice and not self.speaking: # Silence and no one has started talking, so we pre-buffer blocks
9      self.preBufferedBlocks.appendleft(indata)
10     return
11
12  if voice:  # User speaking
13     if not self.speaking: # Recording starts now, so we append the pre-buffered blocks.
14         while len(self.preBufferedBlocks) > 0:
15             np.concatenate((self.buffer, self.preBufferedBlocks.pop()))
16
17     self.buffer = np.concatenate((self.buffer, indata)) # add the block to the buffer
18     self.waiting = EndBlocks
19     self.speaking = True
20  else:  # Silence after user has spoken
21      self.waiting -= 1
22      if self.waiting < 1:
23         self._save_to_process() # Send buffer to transcription queue
24         return
25      else:
26         self.buffer = np.concatenate((self.buffer, indata))

Welche Modelle eignen sich für den Raspberry Pi 4?

Die Laufzeit von Whisper, d.h. die eigentliche Transkription, wird stark von der Wahl des Modells beeinflusst. Insgesamt gibt es fünf Modellgrößen, die einen Kompromiss aus Geschwindigkeit und Genauigkeit bieten. Das heißt, dass das kleinste Modell am schnellsten arbeitet, aber dabei ungenauer ist. Die mangelnde Genauigkeit kann sich dann beispielsweise in fehlenden oder falschen Wörtern äußern.

Eine kurze Laufzeitanalyse hat mir gezeigt, welche Modelle für den Raspberry Pi 4 geeignet sind. Da ein Live-Szenario nie vollständig reproduzierbar ist, habe ich für meine Analyse einen zufällig ausgewählten englischsprachigen Podcast (Länge 100 Sekunden) hergenommen. Die Transkription wurde pro Modell dreimal mit dem Kommandozeilenprogramm “time” gemessen und gemittelt, um Ungenauigkeiten auszugleichen. Die Transkription wurde auf der CPU durchgeführt und der Raspberry Pi 4 im Headless-Modus ohne GUI betrieben, um mögliche Störeinflüsse zu vermeiden.

Ergebnisse der Laufzeitanalyse:

Auf dem Raspberry Pi 4 übersteigt die Laufzeit ab dem Small-Modell die Echtzeit. Daher kommen in diesem Fall für die Live-Transkription nur die Modelle Tiny und Base in Frage. Das Tiny-Modell bietet eine schnellere Live-Transkription auf Kosten der Genauigkeit. Im Gegensatz dazu ist das Base-Modell genauer, jedoch verlangsamt sich die Live-Transkription bereits hier spürbar.

Fazit und Ausblick

Für Heimanwender können die Laufzeit und die Genauigkeit mit dem Tiny- oder dem Base-Modell zufriedenstellend sein. Die größeren Modelle bieten eine höhere Genauigkeit, aber der Raspberry Pi 4 besitzt nicht genügend Leistung, um schnell genug für eine Live-Anwendung zu transkribieren. Im Hinblick auf mögliche kommerzielle Endkundenlösungen ist eine Live-Transkription auf einem Raspberry Pi 4 daher wahrscheinlich keine Option.

Es gibt jedoch meiner Ansicht nach eine Reihe von Ansätzen, wie man an diesem Punkt weitergemachen könnte:

  • Softwareseitig ist die Transkription mit Faster-Whisper bereits effizienter als mit OpenAI Whisper und wird in Zukunft sicherlich noch weiterentwickelt. Daher wäre es naheliegend, einfach z.B. einen Raspberry Pi 5 zu verwenden, der etwa 2–3-mal leistungsfähiger ist.
  • Ein anderer Ansatz wäre die GPU-basierte Transkription zu testen. Eine GPU-basierte Transkription ist allerdings auf CUDA fähige GPUs von Nvidia beschränkt. Der Jetson-Nano von Nvidia könnte hier eine gute Option sein und sollte die Transkriptionszeit um einiges verkürzen.
  • Wenn die Live-Transkription speziell für eine Sprachsteuerung in einem Embedded Device verwendet werden soll, wäre es sinnvoll, eine Wake-Word-Engine zu verwenden, um die Sprachaufzeichnung mit einem Schlüsselwort/-phrase zu starten, anstelle von RMS und Frequenz. Dies sollte die Stabilität noch einmal deutlich verbessern.

In meinem nächsten Experiment werde ich einen dieser Ansätze genauer untersuchen.


Über den Autor

Marno Janetzky

Smart Devices

Marno Janetzky ist seit November 2020 bei MaibornWolff und ist als Senior Software Engineer im Bereich Smart Devices tätig. Im Laufe seiner Karriere hat er sich einen breiten TechStack angeeignet, welches er gekonnt in Projekten einsetzt, um innovative Lösungen für unsere Kunden zu entwickeln. Seine Leidenschaft liegt auf der Embedded Entwicklung unter Linux mit C++ und C, wobei er auch viel Spaß an Java und Kubernetes hat und gerne neue Technologien austestet.