Wir haben eine Diktat-App gebaut, die in jedes Fenster tippt
Wir haben eine Echtzeit-Sprache-zu-Text Desktop-App in Python gebaut, die offline funktioniert und Tastatureingaben direkt in jedes aktive Fenster einfügt.
Die meisten Diktat-Tools kippen Text in ihr eigenes Fenster. Du transkribierst, dann kopierst du alles irgendwo anders hin. Das unterbricht deinen Arbeitsfluss. Tacet hört auf dein Mikrofon und tippt direkt in die App, die gerade im Fokus ist. VS Code, Slack, dein Browser, ein Terminal. Keine Zwischenablage, keine Plugins, kein Fensterwechsel.
Bestehende Optionen binden dich entweder an ein Ökosystem (Apple Dictation, Google Voice Typing) oder brauchen app-spezifische Integrationen. Wir wollten etwas, das überall funktioniert, offline laufen kann, wenn dir Datenschutz wichtig ist, und keine Internetverbindung oder API-Key braucht. Nichts auf dem Markt konnte alle drei Dinge.
Wer das wirklich benutzt
Leute, die viel tippen und lieber reden würden. Autoren, die lange Texte verfassen. Entwickler, die Kommentare oder Dokumentation diktieren wollen, ohne ihren Editor zu verlassen. Jeder mit RSI oder Barrierefreiheits-Bedürfnissen, der nicht den ganzen Tag tippen kann. Leute, denen Datenschutz wichtig ist und die nicht wollen, dass ihre Sprachdaten auf einem Server landen.
Der Tech-Stack
Python 3.9+ mit CustomTkinter für die Desktop-GUI. faster-whisper für lokale Transkription (läuft auf CTranslate2). sounddevice für Echtzeit-Mikrofon-Aufnahme. pynput für globale Hotkeys und Tastatureingabe-Injektion. numpy für Audio-Signalverarbeitung. pystray für das System-Tray-Icon. OpenAI API und Deepgram API als optionale Cloud-Anbieter.
Nichts Ausgefallenes. Wir haben Python gewählt, weil das KI/ML-Ökosystem dort ist. faster-whisper ist eine C++-Whisper-Implementierung mit Python-Bindings, also ist die Transkriptionsgeschwindigkeit kein Python-Problem. Die GUI ist der einzige Teil, der “langsames Python” ist. Und sie muss nicht schnell sein.
Der schwierigste Teil: Live-Vorschau
Wenn du sprichst, zeigt Tacet eine Echtzeit-Vorschau dessen, was es glaubt, dass du sagst. Whisper transkribiert dasselbe Audio-Fenster alle 900ms neu. Der naive Ansatz: die alte Vorschau löschen, die neue tippen. Das verursacht sichtbares Flackern, weil du 40+ Zeichen mehrmals pro Sekunde löschst und neu tippst.
Wir haben es mit stable-prefix diffing gelöst. Bei jedem Update finden wir den längsten gemeinsamen Präfix zwischen altem und neuem Text. Dann löschen wir nur den Suffix, der sich geändert hat, und tippen den neuen Suffix. Wenn Whisper von “Ich möchte” zu “Ich möchte über” wechselt, fassen wir die ersten 11 Zeichen nicht an. Wir hängen nur ” über” an.
# Instead of backspacing the entire preview and retyping,
# we find what's already correct and only fix the tail.
common_len = 0
limit = min(len(old_text), len(new_text))
while common_len < limit and old_text[common_len] == new_text[common_len]:
common_len += 1
# Only delete what changed
old_suffix_len = len(old_text) - common_len
backspace(self.kb, old_suffix_len)
# Only type what's new
new_suffix = new_text[common_len:]
if new_suffix:
self._safe_type(new_suffix)
Das hat den sichtbaren Tastatureingabe-Aufwand um etwa 80% reduziert und die Vorschau fühlt sich flüssig statt ruckelig an.
Das andere Problem war Voice Activity Detection. Ein fester Energieschwellenwert funktioniert nicht, weil jedes Mikrofon unterschiedliche Hintergrundgeräusche hat. Am Ende haben wir einen Noise Floor mit einem exponentiellen gleitenden Durchschnitt getrackt. Dann haben wir Sprach-/Stille-Schwellenwerte als Multiplikatoren über diesem Floor gesetzt. Ein leiser Raum und ein lautes Café funktionieren beide, ohne dass der Benutzer irgendwelche Einstellungen anfasst.
# Fixed thresholds break in noisy rooms. Instead, we track
# the ambient noise level and set thresholds relative to it.
if noise_floor < 1e-6:
noise_floor = rms
else:
noise_floor = noise_alpha * rms + (1 - noise_alpha) * noise_floor
speech_threshold = max(self.energy_threshold, noise_floor * noise_speech_mult)
silence_threshold = max(self.energy_threshold, noise_floor * noise_silence_mult)
if rms >= speech_threshold:
self._speaking = True
Wie die Daten fließen
Drei Threads. Der Hauptthread läuft die GUI. Ein Hintergrund-Thread nimmt Audio vom Mikrofon auf und führt Voice Activity Detection durch. Er teilt Audio anhand von Stille-Pausen in Chunks. Wenn ein Chunk fertig ist, kommt er in eine Queue. Zwei Transkriptions-Worker-Threads ziehen aus dieser Queue und schicken das Audio durch die gewählte Engine (lokales Whisper, OpenAI API oder Deepgram API).
Transkribierter Text fließt durch eine fünfstufige Verarbeitungs-Pipeline: Sprachbefehle (“Punkt” wird zu ”.”), Zeitstempel, Template-Expansion, automatische Großschreibung und Wort-Ersetzungen. Der fertige Text wird über pynput als Tastatureingaben in das aktive Fenster injiziert.
Die Engine und GUI sind über Callbacks entkoppelt. Die Engine importiert nichts aus der GUI. Sie ruft einfach on_status_change, on_preview_update, on_final_text auf. Die GUI verbindet diese mit ihrer Update-Queue. Du könntest die Engine ohne Oberfläche laufen lassen, ohne eine Zeile zu ändern.
Was die App tatsächlich kann
Funktioniert mit jeder Anwendung
Tacet tippt direkt in das Fenster, das gerade im Fokus ist. Es verwendet pynput, um Tastatureingaben auf Betriebssystem-Ebene zu injizieren. Deshalb funktioniert es mit jeder App, die Tastatureingaben akzeptiert. Keine Browser-Extensions, keine API-Integrationen, keine Plugins. Öffne deine E-Mail, fang an zu diktieren, und die Wörter erscheinen. Wechsle zu Slack, rede weiter. Sonderzeichen wie Zeilenumbrüche und Tabs werden durch Drücken der echten Tasten (Enter, Tab) erzeugt, statt Escape-Sequenzen zu tippen.
Adaptive Voice Activity Detection
Die App erkennt automatisch, wann du anfängst und aufhörst zu sprechen. Sie trackt Umgebungsgeräusche mit einem exponentiellen gleitenden Durchschnitt. Dann setzt sie Sprach-Erkennungs-Schwellenwerte als Multiplikatoren über diesem Noise Floor. Das funktioniert in einem leisen Raum genauso wie in einer lauten Umgebung, ohne manuelle Kalibrierung. Außerdem puffert sie 300ms Audio vor der erkannten Sprache (Preroll), damit du nie die erste Silbe eines Satzes verlierst.
Offline-first mit Cloud-Fallback
Die Standard-Engine ist faster-whisper, lokal auf deiner CPU. Keine API-Keys, kein Internet, keine Daten, die deinen Rechner verlassen. Beim ersten Start führt Tacet einen Geschwindigkeitstest mit dem gewählten Modell durch. Wenn es länger als 5 Sekunden braucht, um 1 Sekunde Audio zu transkribieren, wechselt es automatisch zum “tiny”-Modell und speichert diese Einstellung. Wenn du höhere Genauigkeit willst und Cloud okay ist, kannst du mit einer Konfig-Änderung zu OpenAI oder Deepgram wechseln.
Anpassbare Sprachbefehle und Textverarbeitung
Sag “Punkt” und es tippt ”.”. Sag “neue Zeile” und es drückt Enter. Sag “lösch das” und es löscht den letzten Chunk per Backspace. All das ist konfigurierbar. Du kannst eigene Befehle hinzufügen, eingebaute deaktivieren oder Wort-Ersetzungen einrichten (“btw” wird zu “by the way”). Text fließt durch eine fünfstufige Pipeline: Sprachbefehle, Zeitstempel, Templates, automatische Großschreibung und Ersetzungen. Jede Stufe ist unabhängig und kann ein- oder ausgeschaltet werden.
Kompromisse, die wir gemacht haben
Wir haben CustomTkinter statt Electron gewählt. Electron hätte uns mit weniger Aufwand eine hübschere UI gegeben. Aber es liefert einen ganzen Chromium-Browser mit. Für eine App, die leise im System-Tray sitzt und wenig Ressourcen braucht, fühlte sich das falsch an. CustomTkinter ist leichter, startet schneller und hält die Installationsgröße klein. Der Kompromiss sind weniger UI-Komponenten und weniger visueller Feinschliff.
Wir haben regelbasierte Textverarbeitung gewählt, statt ein LLM für die Nachbearbeitung zu verwenden. Ein LLM könnte Grammatik korrigieren, klügere Zeichensetzung hinzufügen, vielleicht sogar Sätze umstrukturieren. Aber es würde Latenz hinzufügen, entweder einen Cloud-Aufruf oder ein zweites lokales Modell erfordern, und die Ausgabe unvorhersehbar machen. Wenn jemand in ein Chat-Fenster diktiert, will er seine Wörter jetzt getippt haben, nicht zwei Sekunden später korrigiert. Regeln sind schnell, vorhersagbar und kostenlos.
Wir haben auf Echtzeit-Streaming-Transkription für die lokale Engine verzichtet. Whisper arbeitet mit vollständigen Audio-Chunks, nicht mit Streams. Wir hätten eine Streaming-Pipeline mit überlappenden Fenstern bauen können. Aber die Komplexität war es nicht wert. Stattdessen haben wir das Chunking optimiert (Stille-Erkennung, Preroll-Pufferung, minimale Chunk-Dauer), damit es sich reaktionsschnell genug anfühlt. Die meisten Äußerungen werden in unter einer Sekunde auf einer vernünftigen CPU verarbeitet.
Wir haben auch Tastatureingabe-Injektion statt Zwischenablage gewählt. Zwischenablage-Injektion wäre einfacher und plattformübergreifend zuverlässiger. Aber sie überschreibt, was der Benutzer zuletzt kopiert hat. Das ist ein Dealbreaker für die meisten Arbeitsabläufe.
Was wir anders machen würden
Das Konfig-System hat als flache JSON-Datei angefangen und ist organisch gewachsen. Am Ende hatten wir Legacy-Format-Migrationscode für Wort-Ersetzungen, weil die GUI eine andere Struktur brauchte als die ursprüngliche Konfig. Wenn wir nochmal anfangen würden, hätten wir von Tag eins ein ordentliches Schema mit Versionierung definiert, statt nachträglich Migrations-Logik dranzubauen.
Das Threading-Modell funktioniert, aber es ist handgestrickt mit Locks, Events und Utterance-IDs für Stale-Detection. Eine Library wie concurrent.futures oder ein async-Ansatz mit asyncio wäre sauberer gewesen. Wir würden auch in Betracht ziehen, die Engine in einen eigenen Prozess auszulagern, um den GIL komplett zu umgehen. Audio-Aufnahme und Transkription sind beide CPU-gebunden.
Wir würden auch früher in automatisierte Tests investieren. Die Callback-basierte Architektur macht die Engine theoretisch testbar. Aber wir haben keine Test-Suite, die die gesamte Pipeline durchläuft. Das ist technische Schuld, die wir mit uns tragen.