Für meine Diktier-App VoiceFlow brauchte ich eine Sache, die überraschend schwer zu bekommen ist: schnelle, lokale Spracherkennung auf einem Rechner ohne NVIDIA-Karte. Die meisten guten STT-Stacks gehen stillschweigend von CUDA aus. Ich habe hier aber eine AMD/Intel-GPU — und genau dafür gibt es einen unterschätzten Weg: ONNX-Runtime mit dem DirectML-Execution-Provider. Das nutzt jede DirectX-12-GPU, egal von welchem Hersteller.
Das Setup: Parakeet TDT 0.6b v3 via onnx-asr
Mein Modell der Wahl ist NVIDIAs Parakeet TDT 0.6b v3 — ironischerweise ein NVIDIA-Modell, das hier komplett ohne NVIDIA-Hardware läuft. Es ist multilingual (Deutsch + Englisch mit Auto-Erkennung), was für mich entscheidend war, weil ich beim Diktieren ständig zwischen den Sprachen wechsle.
Die Anbindung übernimmt das Paket onnx-asr. Die ganze Engine ist erfreulich kurz. Der Kern sieht so aus:
MODEL_ID = "nemo-parakeet-tdt-0.6b-v3"
DEFAULT_PROVIDERS = ["DmlExecutionProvider", "CPUExecutionProvider"]
import onnx_asr
self.model = onnx_asr.load_model(
MODEL_ID, quantization=quantization, providers=self.providers
)
Der providers-Trick ist die ganze Magie: DmlExecutionProvider zuerst, CPUExecutionProvider als Fallback. ONNX-Runtime nimmt den ersten, der auf der Maschine verfügbar ist. Geht DirectML aus irgendeinem Grund nicht, fällt es still auf die CPU zurück — die App stürzt nicht ab, sie wird nur langsamer.
Ein Detail aus der transcribe-Methode, das ich erst nach einem kryptischen Fehler eingebaut habe: Das Audio muss zwingend ein zusammenhängendes float32-Array sein, sonst meckert die Runtime.
audio = np.ascontiguousarray(audio, dtype=np.float32)
if audio.ndim > 1: # versehentliche (n, 1)-Formen platt machen
audio = audio.reshape(-1)
sounddevice liefert je nach Konfiguration gern mal eine (n, 1)-Form statt (n,). Das reshape(-1) macht das platt, bevor es Probleme gibt.
Die Zahlen: 600 MB einmal, dann 0,3 Sekunden
Beim allerersten Start lädt Parakeet einmalig ~600 MB von Hugging Face und cacht das Modell danach lokal. Das ist eine spürbare Wartezeit beim ersten Lauf, aber eben nur einmal.
Danach ist die Latenz das eigentliche Verkaufsargument: warm rund 0,3 Sekunden pro Diktat. Push-to-talk Taste loslassen, kurz blinzeln, Text ist da. Für eine Diktier-App, die sich nicht im Weg stehen darf, ist das genau die Schwelle, ab der es sich „instant" anfühlt.
Zum Verifizieren habe ich einen kleinen Smoke-Test (_smoke()) in die Datei gepackt, der ganz ohne Mikrofon auskommt — er feuert 2 Sekunden leises Rauschen durch die Pipeline und misst Load- und Inferenzzeit getrennt:
audio = (rng.standard_normal(SAMPLE_RATE * 2) * 0.01).astype(np.float32)
out = engine.transcribe(audio)
So konnte ich den Load+Inferenz-Pfad schon validieren, bevor überhaupt Audio-Capture stand. Solche De-Risk-Tests am Anfang eines Milestones haben mir hier viel Frust erspart.
Der Stolperstein, der mich am meisten gekostet hat
Wenn ihr nur eine Sache aus diesem Post mitnehmt, dann diese:
Installiert nur EINE onnxruntime-Variante. Niemals
onnxruntimenebenonnxruntime-directml.
Beide Pakete liefern dasselbe Python-Modul onnxruntime. Hat man beide im venv, gewinnt mal das eine, mal das andere — und plötzlich ist der DmlExecutionProvider nicht mehr da, alles läuft still auf der CPU, und man wundert sich, warum „die GPU-Beschleunigung" auf einmal weg ist. Der Fehler ist heimtückisch, weil nichts crasht. Es wird einfach langsam.
Zweiter Versions-Fallstrick, den ich mir notiert habe: onnxruntime-directml 1.24.1 funktioniert nicht — es braucht mindestens 1.24.2. Bei 1.24.1 sprang der DirectML-Provider bei mir nicht an. Eine Minor-Patch-Version Unterschied, und der ganze GPU-Pfad ist tot.
Fazit
Der Stack — onnx-asr + onnxruntime-directml + Parakeet TDT 0.6b v3 — gibt mir lokale, mehrsprachige Spracherkennung mit ~0,3 s Latenz auf Hardware, die offiziell „kein KI-Rechner" ist. Kein CUDA, keine Cloud, keine API-Kosten, alles bleibt auf dem Gerät.
Die Lehre: Die Technik selbst ist erstaunlich unkompliziert — die echte Arbeit steckt in der Paket-Hygiene. Eine saubere requirements.txt mit genau einer onnxruntime-Variante und gepinnter Mindestversion ist hier wichtiger als jede Codezeile.