Steven
Steven6 Min. Lesezeit

Wir löschten 5,000 Zeilen Audio-Code — und Transkripte tauchten plötzlich doppelt auf

GeekBye v1.6 riss zwei on-device-Swift-Transkriber heraus und ersetzte sie durch eine einheitliche Pipeline — ein Netto-Löschen von über 5,000 Zeilen, während Nutzer gerade mitten in Meetings waren. Dann tauchten Transkripte doppelt auf, weil der Fehler in die eine Schicht wanderte, die noch glaubte, es gäbe zwei Engines.

Engineering
Transkription
Architektur
GeekBye-Releases
Wir löschten 5,000 Zeilen Audio-Code — und Transkripte tauchten plötzlich doppelt auf

Es gibt eine besondere Art von Schrecken beim Refactoring von Code, der gerade jetzt läuft — für Menschen, die mitten im Meeting sind. Man kann das Feature nicht für Wartungsarbeiten abschalten — jemand verlässt sich darauf, um ein Gespräch zu transkribieren, das er nicht wiederholen kann. GeekBye v1.6 hat genau das mit dem kritischsten Pfad in der App getan: der Audio-Pipeline. Das ist die Geschichte davon, was wir gelöscht haben, was kaputt ging, und die zwei Zuverlässigkeitslektionen, die dabei herauskamen.

Zwei Engines, eine davon in Swift

Vor v1.6 lief GeekByes Speech-to-Text on-device in Swift-Binaries. Es gab zwei davon: eine, die Apples Speech-Framework kapselte, und eine größere (über 1,500 Zeilen), die duales Audio erfasste — Mikrofon plus System-Audio — über ScreenCaptureKit und ihren eigenen WebSocket direkt zu unserem Backend öffnete, um an Deepgram zu streamen. Zwei Transkriber, zwei Capture-Pfade, zwei Wege, mit dem Netzwerk zu sprechen — alles in einer kompilierten Sprache, was bedeutete, dass jede Änderung eine Neukompilierung der Binary erforderte.

v1.6.0 kollabierte alles zu einer Pipeline. Der einzelne vereinheitlichende Commit löschte den Apple Speech-Transkriber (~1,145 Zeilen), den Deepgram Swift-Capturer (~1,523 Zeilen), beide TypeScript-Bridges, den alten Orchestrator und die Dual-Audio-IPC-Handler — ein Netto-Löschen von über 5,200 Zeilen. An ihrer Stelle: Der Renderer erfasst Audio über standard Web APIs, schickt es als PCM über IPC, und der Electron-Hauptprozess besitzt einen einzigen WebSocket-Pfad zum Backend, mit dem Provider, der durch die Backend-Konfiguration gewählt wird. Ein Capture-Pfad, ein Socket-Owner, ein Ort, an dem man nachdenken muss.

Fünftausend Zeilen zu löschen fühlt sich großartig an. Für etwa einen Tag.

Der Fehler wanderte in die Schicht, die noch glaubte, es gäbe zwei

Dann begannen Transkripte doppelt aufzutauchen. Ein einzeln gesprochener Satz wurde als zwei identische Einträge gespeichert — manchmal noch schlimmer.

Die Ursache ist das Lehrreichste in diesem gesamten Release, weil es ein allgemeines Gesetz der Vereinheitlichung ist. Wir hatten die Provider im Hauptprozess vereinheitlicht — ein Socket, ein Handler. Aber die React-Schicht hatte die Nachricht nicht erhalten: Sie montierte noch zwei anbieterspezifische Event-Hooks gleichzeitig, einen für Deepgram und einen für ElevenLabs. Beide hörten zu. Beide speicherten. Jedes Transkript wurde von den Hooks persistiert, die noch aktiv waren — und das waren sie beide.

Und darunter lag eine zweite, heimtückischere Verdoppelung: ElevenLabs emittiert jede finalisierte Zeile zweimal — einmal als committed-Event und erneut als committed_with_timestamps. Selbst mit einem einzigen Hook konnte also ein Satz als zwei Events ankommen.

Der Fix (in der v1.6-Linie) ist ein aufgeräumter Zweiteiler, der genau auf die beiden Ursachen abbildet: Jeden Event-Hook hinter einem enabled-Flag sperren, das durch die aktiv-Provider-Konfiguration des Backends gesteuert wird, damit immer nur ein Listener aktiv ist; und quellenbasierte Text-Deduplizierung hinzufügen (die letzte gespeicherte Zeile für Mikrofon und für System-Audio merken), um ElevenLabs' Doppel-Emit zu schlucken.

Die Lektion ist größer als dieser Fehler: Wenn man zwei Backends hinter einer Schnittstelle vereinheitlicht, verschwindet die Verdoppelung nicht — sie wandert in die Schicht, die noch davon ausgeht, dass es zwei gibt. Wir legten die Leitungen im Hauptprozess zusammen und das Leck tauchte zwei Schichten höher auf, in der Event-Verdrahtung der UI. Eine Pipeline zu vereinheitlichen ist nicht erledigt, wenn der Producer vereinheitlicht ist; es ist erledigt, wenn jeder Consumer aufgehört hat, an die alte Form zu glauben.

Der erste Reconnect: „nach fünf Versuchen aufgeben"

v1.6.0 lieferte auch GeekByes allerersten WebSocket-Auto-Reconnect mit exponentiellem Backoff — und es lohnt sich, ihn genau vorzustellen, weil er der primitive Vorfahre der kugelsicheren Version ist, die wir heute betreiben.

Der ursprüngliche war begrenzt. Fünf Versuche, Verzögerungen von 1s, 2s, 4s, 8s, 16s — und dann kapitulierte er, indem er einen fatalen Fehler emittierte: „Connection lost — please restart transcription." Damals fühlte sich das verantwortungsvoll an: nicht ewig wiederholen, dem Nutzer ehrlich sagen. In der Praxis war es ein UX-Fehler im Verborgenen. Ein sechzehnsekündiger Netzaussetzer — ein WLAN-Handoff, ein VPN-Reconnect, ein Tunnel — ist transient, aber ein begrenzter Retry behandelt ihn als terminal. Der Nutzer hatte nichts falsch gemacht und wurde gebeten, ein laufendes Meeting neu zu starten.

Das ist genau das Versagen, das das aktuelle Design eliminiert hat. Heute reconnected GeekBye mit unbegrenztem Backoff und wechselt sogar Provider, und stoppt nur bei einem wirklich fatalen Grund — Auth, Quota, Billing. Die gesamte Reise von „nach fünf aufgeben" zu „bei einem transienten Fehler niemals aufgeben" wird in warum dein KI-Notiztool bei schlechtem WLAN stehen bleibt erzählt. v1.6 ist der Punkt, an dem diese Straße begann, mit der naiven Version, die zuerst existieren musste.

Veraltete Verbindungen verfolgen dich, wenn sie keine Namen haben

Eine weitere Zuverlässigkeitslektion landete in v1.6.3, die eine subtile Klasse von Fehlern behebt, auf die jeder stoßen wird, der Reconnection über ein zustandsbehaftetes Backend macht.

Wenn man reconnectet — oder mitten in einer Sitzung die Transkriptionssprache wechselt, was intern ebenfalls reconnectet — stirbt die alte Verbindung nicht immer leise. Ihre Todesstoß-Nachrichten (CONNECTION_LOST, disconnected) können nachdem die neue Verbindung bereits gesund ist ankommen und den einwandfrei funktionierenden Nachfolger einreißen. Der Fix gibt jedem Verbindungsversuch eine Identität — eine pro-Verbindung-Session-ID, die in die WebSocket-URL gestempelt ist — plus ein kurzes Toleranzfenster nach dem Reconnect, während dessen veraltete Disconnect-Nachrichten von einem vorherigen Versuch ignoriert werden. Außerdem wird eine explizite stop-Nachricht gesendet, bevor ein Socket geschlossen wird, damit das Backend die alte Sitzung sauber abbauen kann.

Das Prinzip: Jede Reconnect-Logik über ein zustandsbehaftetes Backend muss jeden Versuch mit einer Identität taggen und späte Nachrichten von vorherigen Versuchen als Rauschen behandeln — sonst wird ein erfolgreicher Reconnect vom letzten Atemzug seines Vorgängers getötet.

Drei Dinge, die uns v1.6 lehrte

  1. Vereinheitlichung verschiebt Fehler; sie löscht sie nicht. Zwei Engines zu einer zusammenfassen und jeden Consumer überprüfen, der noch glaubt, es gibt zwei. Bei uns war es die UI, die zwei Event-Hooks montierte, nachdem die Leitungen bereits zusammengelegt waren.
  2. Der erste Reconnect wird begrenzt sein, und begrenzt ist falsch für Live-Streams. „Nach N Versuchen aufgeben" verwandelt einen transienten Aussetzer in ein terminales Versagen, für das der Nutzer verantwortlich gemacht wird. Fatal (Auth, Quota) von transient (ein abgebrochener Socket) von der allerersten Version an unterscheiden.
  3. Reconnections brauchen Identität. Jeden Versuch taggen; späte Nachrichten von toten Versuchen ignorieren. Eine Verbindung ohne Namen kann von ihrem eigenen Geist ermordet werden.

Das ist das zweite Kapitel der Zuverlässigkeitsgeschichte, die GeekBye v2 wird. Für das erste, siehe die Auto-Update-Saga, die sechs Releases in vier Tagen brauchte (v1.5.x); für den Punkt, an dem die Reconnect-Arbeit schließlich ankam, warum dein KI-Notiztool bei schlechtem WLAN stehen bleibt; und für den gesamten Bogen, die Anatomie des perfekten Software-Auslieferns.

Ähnliche Artikel

Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es
Steven
Steven6 Min. Lesezeit

Eine abgebrochene Verbindung sollte nicht die ganze App zum Absturz bringen — unsere tat es

Als unser Backend mitten in einem Meeting offline ging, hat es nicht nur die Transkription pausiert — es hat die ganze App zum Absturz gebracht. Die Ursache war ein einziges nicht behandeltes Event, und der Fix waren zehn Zeilen. Das ist das Release-Cluster, das GeekBye angemeldet bleiben und verbunden bleiben ließ, durch genau die Dinge, die es früher umgebracht haben.

Engineering
Zuverlässigkeit
Electron
Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log
Steven
Steven7 Min. Lesezeit

Eine CSS-Variable, fünf Review-Runden und eine Swift-Toolchain, die log

GeekBye v2.0.7 machte das gesamte Overlay verstellbar durchscheinend — was nach einer einzeiligen CSS-Änderung klingt und definitiv keine war. Die eigentliche Geschichte ist, was das Code-Review aufdeckte: zwei Flächen, die sich weigerten zu verblassen, eine Aufnahmeleiste, die bei gleicher Deckkraft falsch aussah, und ein kompiliertes Binary, das um 672 Bytes hin und her sprang, weil unsere eigene Dokumentation einem Reviewer die falsche Swift-Version nannte.

Engineering
Design
Build
Die Sicherheitsprüfung, die unsere App unmöglich zu beenden machte
Steven
Steven6 Min. Lesezeit

Die Sicherheitsprüfung, die unsere App unmöglich zu beenden machte

Das automatische Update war das schwerste Feature, das wir je ausgeliefert haben — sechs Releases in vier Tagen, damit es die App nicht mehr unbrauchbar machte. Der schlimmste Bug war einer, den wir aus lauter Vorsicht selbst eingebaut haben: eine 500-Millisekunden-„Sicherheits"-Prüfung, die aus einem fehlgeschlagenen Update einen Prozess machte, den man buchstäblich nicht beenden konnte.

Engineering
Electron
Zuverlässigkeit