
Die Stille war tragend
Die letzten beiden Releases von GeekBye v1 drehen sich um dieselbe unbequeme Wahrheit: Echtzeit-Transkription über ein echtes Netzwerk ist nicht verlustfrei, und der ehrliche Schritt ist, aufzuhören, so zu tun, als wäre sie es. v1.8.20 behielt eine Kopie jedes Audio-Chunks auf der Festplatte, bevor es ihn während eines Reconnects verwarf, und begann, die Lücken im Transkript laut zu markieren. v1.9.0 hörte auf, Stille zu senden, um Bandbreite zu sparen — und entdeckte, dass die Stille genau das Signal war, mit dem der Transkriber erkannte, dass ein Satz zu Ende war. Zwei Releases über die Kosten des Wegwerfens.
Die letzten beiden Releases von GeekBye v1 — v1.8.20 und v1.9.0, die direkt vor dem Meilenstein der Version 2 — teilen ein Thema, das erst offensichtlich wird, wenn man sie zusammen liest. Beide handeln von Dingen, die die App still wegwarf, und beide kommen zum selben Schluss: das, was du für Effizienz wegwirfst, tat mehr Arbeit, als du dachtest. Ein Release warf Audio während Netzwerk-Reconnects weg. Das andere warf Stille weg, um Bandbreite zu sparen. In beiden Fällen erwies sich das Weggeworfene als tragend.
Echtzeit-Transkription ist nicht verlustfrei, und v1.8.20 gibt es zu
Hier ist eine Tatsache über Streaming-Speech-to-Text über einen WebSocket, von der man leicht wegschaut: wenn die Verbindung abbricht und sich wiederverbindet — oder wenn die App von einem Transkriptions-Anbieter zu einem anderen umschaltet, von Deepgram zu ElevenLabs — verwirft der Client absichtlich die während dieser Lücke erfassten Audio-Chunks. Er muss. Ein frisch verbundener Anbieter, der plötzlich einen Rückstau veralteten Audios empfängt, verschluckt sich, oder schlimmer, verhakt seinen Sprachaktivitäts-Detektor an der falschen Sache. Also wirft der Client diese Chunks weg.
Das Problem ist nicht das Verwerfen. Das Problem ist, was das Verwerfen früher bedeutete: diese Wörter tauchten schlicht nie im Transkript auf, und nichts sagte dir, dass sie fehlten. Die Aufnahme sah vollständig aus. Sie war es nicht.
v1.8.20 greift das an zwei Fronten an, und keine davon ist „Reconnects nie Audio verlieren lassen“ — denn das kannst du nicht, nicht in Echtzeit. Stattdessen macht es den Verlust wiederherstellbar und ehrlich.
Die wiederherstellbare Hälfte ist ein dauerhafter PCM-Tee. Im Erfassungs-Handler des Hauptprozesses wird jetzt jeder erfasste Chunk in eine Datei auf der Festplatte geschrieben, bevor er die Verwerf-Gates von Reconnect und Gnadenfrist erreicht:
every captured chunk is written to
<userData>/sessions/session-<ts>.wavbefore realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass
Der Mechanismus ist erfreulich Low-Tech: teeChunkToDisk öffnet einen WriteStream, reserviert vorab einen 44-Byte-WAV-Header, hängt rohe 16-Bit-Mono-Samples an, sobald sie ankommen, und flickt den Header beim Stoppen mit den finalen Größen (fs.openSync(..., 'r+'), 44 Bytes an Offset 0 schreiben). Das Ergebnis ist ein abspielbares WAV des vollständigen Audios — inklusive der Teile, die das Echtzeit-Transkript nie sah. Wenn die Aufnahme endet, emittiert es ein session-audio-saved-Ereignis mit dem Dateipfad und der Byte-Zahl.
Zwei Ehrlichkeitsnotizen zählen hier, denn es ist leicht, das zu übertreiben. Erstens: der Tee füllt die Lücke in diesen Releases nicht. Er bewahrt das rohe Audio für einen späteren Abgleichlauf — eine Batch-Neu-Transkription, die die Datei gegen das Live-Transkript abgleicht — was ein Haken ist, den diese Releases setzen, kein Feature, das sie ausliefern. Der Folge-Commit versieht session-audio-saved sogar mit einem TODO, das auf die Abgleich-Tickets zeigt, damit niemand es für toten Code hält. Zweitens: das kostet Festplatte. Das Team schätzte grob 86 MB pro 30-Minuten-Aufnahme, also fügt dasselbe Release einen StartupCleanupService hinzu, der gespeicherte Sitzungen beschneidet, die älter als 7 Tage sind. Ein Sicherheitsnetz, das grenzenlos wächst, ist ein Leck; das Aufräumen ist das, was den Tee auslieferbar macht.
Die ehrliche Hälfte ist noch einfacher. Beim ersten Reconnect-Versuch schreibt der Client einen echten, sichtbaren Eintrag ins Transkript:
[reconnecting — audio may be missing]
Diese eine Zeile ist die ganze Philosophie des Releases im Kleinen. Die App kann nicht garantieren, dass sie alles gehört hat, also hört sie auf zu suggerieren, dass sie es tat. Eine Lücke, die du sehen kannst, ist eine Lücke, die du umgehen kannst; eine Lücke, die du nicht sehen kannst, ist ein Fehlerbericht, der nur darauf wartet zu passieren.
v1.9.0: die Stille, die du wegoptimiert hast, war das Signal
v1.9.0 ist das letzte v1-Release, und dort reichte das Backend einen Satz Live-Transkript-Verantwortlichkeiten an den Client herunter — Stille verwerfen, jede Transkriptzeile aus einer lokalen Sprachaktivitäts-Zeitleiste dem Mikro oder System-Audio zuordnen, die Reconnect-Schleife besitzen, Aufnahme-Obergrenzen durchsetzen. Das meiste davon ist Vorarbeit für die Architektur der Version 2 und wurde anderswo erzählt. Aber einer seiner Fäden ist die beste kleine Geschichte in einem der beiden Releases, denn es ist ein Fix, der den Schaden einer Optimierung rückgängig machen musste, die für sich genommen vollkommen korrekt war.
Es beginnt mit einer offensichtlich guten Änderung: aufhören, Stille zu senden. Der Client begann, Chunks, deren dominante Quelle Stille war, herauszufiltern, bevor er sie über den Socket schickte — weniger Bandbreite, weniger nutzloses Audio, das das Backend verarbeiten muss, und es ehrte einen expliziten Backend-Client-Vertrag, der sagte „sende keine Stille“. Jeder Grund, es zu tun, war stichhaltig.
Und es brach die Utterance-Finalisierung vollständig. Der Commit, der es behebt, legt die Kausalkette mit ungewöhnlicher Offenheit dar:
After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.
Dies ist die platonische Form einer undichten Abstraktion, die zurückbeißt. Der Sprachaktivitäts-Detektor des Transkribers hat keine separate Eingabe für „der Sprecher ist fertig“ — er schließt das Ende eines Satzes daraus, dass er etwa eine Sekunde Stille im Audio hört. Stille war kein Rauschen auf der Leitung; sie war eine Nachricht. Indem der Client sie herausfilterte, um Bandbreite zu sparen, hörte er versehentlich auf, das eine Signal zu senden, das dem Transkriber sagte, dass ein Satz zu Ende war. Also wuchs jedes partielle Transkript einfach weiter, das Ende verlief in einem ..., das sich nie auflöste.
Du könntest das beheben, indem du die Stille wieder einfügst — aber dann hast du die Optimierung rückgängig gemacht. Der eigentliche Fix ist besser: die Bedeutung der Stille senden, ohne die Stille zu senden. Der Client verfolgt jetzt lokal, wie lange das Audio still war, und wenn das SILENCE_COMMIT_THRESHOLD_MS = 1000 überschreitet — bewusst abgestimmt auf die eigene VAD-Stille-Schwelle des Backends — feuert er ein explizites commit-Signal out of band: sendMixedChunk bemerkt die anhaltende Stille, sendet ein realtime-audio:commit über IPC, der Hauptprozess leitet ein { type: 'commit' } über den Socket weiter, und das Backend weist ElevenLabs an, das Partielle zu finalisieren. Zwei kleine Zustandsstücke, silenceStartedAt und silenceCommitFired, halten es davon ab, wiederholt zu feuern. Der Bandbreiten-Gewinn überlebt; die Finalisierung funktioniert wieder; das nachlaufende ... löst sich auf.
Es gibt auch eine kleine, ehrliche Fußnote zu diesem Release: die grenzenlose Reconnect-Logik, die v1.9.0 einführte, wurde zuerst im Renderer als WsReconnectController gebaut und dann innerhalb desselben Releases gelöscht, sobald die echte Implementierung im Hauptprozess-Handler landete, wo sie hingehörte. Die Reconnect-Geschichte selbst — grenzenloser Backoff, alle fünf Fehlschläge abwechselnde Anbieter — wird in einem eigenen Kapitel erzählt; was v1.9.0 hinzufügt, ist, dass sie dort geboren wurde, und dort der Client zum ersten Mal die Verantwortung dafür übernahm.
Drei Dinge, die diese Releases lehrten
- Wenn du Verlust nicht verhindern kannst, mach ihn wiederherstellbar und mach ihn sichtbar. v1.8.20 hindert Reconnects nicht daran, Audio zu verwerfen — das ist in Echtzeit physisch nicht möglich. Es behält eine Kopie des rohen Audios für später und beschriftet die Lücke im Transkript. Keines von beiden ist glanzvoll; zusammen verwandeln sie stillen Datenverlust in einen bekannten, begrenzten, wiederherstellbaren Zustand.
- Ein Sicherheitsnetz braucht seine eigenen Grenzen. Der dauerhafte Tee würde die Festplatte still füllen, wenn man ihn ließe — ein Fix, der zu einem Zeitlupen-Bug wird. Das 7-Tage-Aufräumen, das im selben Release ausgeliefert wurde, ist kein nachträglicher Einfall; es ist das, was den Tee überhaupt sicher auslieferbar macht. Wenn du einen Mechanismus hinzufügst, der akkumuliert, füge im selben Atemzug den Mechanismus hinzu, der ihn beschneidet.
- Das, was du für Effizienz wegwirfst, ist vielleicht ein Signal. Die schärfste Einzel-Lektion in einem der beiden Releases: Stille herauszufiltern war auf jeder Achse korrekt, die du normalerweise prüfen würdest — Bandbreite, Last, Vertragstreue — und es brach das Produkt trotzdem, weil ein nachgelagertes System Bedeutung in die Stille hineinlas. Wenn du Daten aus Effizienz entfernst, frage, was aus diesen Daten etwas ableitete. Manchmal musst du die Bedeutung explizit senden, gerade weil du aufgehört hast, das zu senden, woraus sie früher abgeleitet wurde.
Und das ist das Ende der Geschichte von Version 1 — die letzte Härtung vor dem Neuschreiben. Für das vorige Kapitel, die drei Verben, die Web Audio am Leben halten (v1.8.16 + v2.0.2); für das Ziel, auf das all dies zusteuerte, was eine Version 2 wirklich kostet: 206 Commits (v2.0.0); und für den ganzen Bogen, die Anatomie, Software bis zur Perfektion auszuliefern.