
Le silence était porteur
Les deux dernières versions de GeekBye v1 tournent autour de la même vérité inconfortable : la transcription en temps réel sur un vrai réseau n'est pas sans perte, et le geste honnête est de cesser de prétendre le contraire. La v1.8.20 gardait sur disque une copie de chaque chunk audio avant de le jeter pendant une reconnexion, et s'est mise à marquer les trous dans la transcription à voix haute. La v1.9.0 a cessé d'envoyer le silence pour économiser de la bande passante — et a découvert que le silence était justement le signal que le transcripteur utilisait pour savoir qu'une phrase s'était terminée. Deux versions sur le coût de jeter les choses.
Les deux dernières versions de GeekBye v1 — la v1.8.20 et la v1.9.0, celles juste avant le jalon de la version 2 — partagent un thème qui ne devient évident que lorsqu'on les lit ensemble. Toutes deux portent sur des choses que l'app jetait en silence, et toutes deux arrivent à la même conclusion : ce que vous jetez pour être efficace faisait plus de travail que vous ne le pensiez. Une version jetait de l'audio pendant les reconnexions réseau. L'autre jetait le silence pour économiser de la bande passante. Dans les deux cas, la chose jetée s'est révélée porteuse.
La transcription en temps réel n'est pas sans perte, et la v1.8.20 l'admet
Voici un fait sur le speech-to-text en streaming sur un WebSocket qu'il est facile de fuir du regard : quand la connexion tombe et se rétablit — ou quand l'app bascule d'un fournisseur de transcription à un autre, de Deepgram à ElevenLabs — le client jette délibérément les chunks audio capturés pendant ce trou. Il le faut. Un fournisseur fraîchement connecté qui reçoit soudain un arriéré d'audio périmé va s'étrangler, ou pire, accrocher son détecteur d'activité vocale à la mauvaise chose. Alors le client jette ces chunks.
Le problème n'est pas le rejet. Le problème est ce que le rejet signifiait : ces mots n'apparaissaient tout simplement jamais dans la transcription, et rien ne vous disait qu'ils manquaient. L'enregistrement avait l'air complet. Il ne l'était pas.
La v1.8.20 attaque cela sur deux fronts, et aucun d'eux n'est « faire que les reconnexions ne perdent jamais d'audio » — parce que vous ne pouvez pas, pas en temps réel. Elle rend plutôt la perte récupérable et honnête.
La moitié récupérable est un tee PCM durable. Dans le handler de capture du processus principal, chaque chunk capturé est désormais écrit dans un fichier sur disque avant d'atteindre les portes de rejet de reconnexion et de période de grâce :
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
Le mécanisme est d'une simplicité agréable : teeChunkToDisk ouvre un WriteStream, réserve d'avance un en-tête WAV de 44 octets, ajoute les échantillons mono bruts sur 16 bits à mesure qu'ils arrivent, et rustine l'en-tête avec les tailles finales à l'arrêt (fs.openSync(..., 'r+'), écrire 44 octets à l'offset 0). Le résultat est un WAV lisible de l'audio complet — y compris les parties que la transcription en temps réel n'a jamais vues. Quand l'enregistrement se termine, il émet un événement session-audio-saved avec le chemin du fichier et le nombre d'octets.
Deux notes d'honnêteté comptent ici, parce qu'il est facile de sur-vendre cela. D'abord : le tee ne comble pas le trou dans ces versions. Il préserve l'audio brut pour une passe de réconciliation ultérieure — une re-transcription par lots qui réconcilie le fichier avec la transcription en direct — ce qui est un point d'accroche que ces versions posent, pas une fonctionnalité qu'elles livrent. Le commit de suivi annote même session-audio-saved d'un TODO pointant vers les tickets de réconciliation pour que personne ne le prenne pour du code mort. Ensuite : cela coûte du disque. L'équipe a estimé environ 86 Mo par enregistrement de 30 minutes, alors la même version ajoute un StartupCleanupService qui élague les sessions sauvegardées de plus de 7 jours. Un filet de sécurité qui grandit sans borne est une fuite ; le nettoyage est ce qui rend le tee livrable.
La moitié honnête est encore plus simple. À la première tentative de reconnexion, le client écrit une entrée réelle et visible dans la transcription :
[reconnecting — audio may be missing]
Cette seule ligne est toute la philosophie de la version en miniature. L'app ne peut pas garantir qu'elle a tout entendu, alors elle cesse de laisser entendre qu'elle l'a fait. Un trou que vous pouvez voir est un trou que vous pouvez contourner ; un trou que vous ne pouvez pas voir est un rapport de bug en puissance.
v1.9.0 : le silence que vous avez optimisé jusqu'à le faire disparaître était le signal
La v1.9.0 est la dernière version v1, et c'est là que le backend a transmis au client un ensemble de responsabilités de transcription en direct — jeter le silence, attribuer chaque ligne de transcription au micro ou à l'audio système à partir d'une chronologie locale d'activité vocale, posséder la boucle de reconnexion, imposer les plafonds d'enregistrement. La plus grande partie de cela est le terrain préparatoire pour l'architecture de la version 2 et a été racontée ailleurs. Mais l'un de ses fils est la meilleure petite histoire de l'une ou l'autre version, parce que c'est un correctif qui a dû défaire les dégâts d'une optimisation parfaitement correcte prise isolément.
Cela commence par un changement manifestement bon : cesser d'envoyer le silence. Le client s'est mis à filtrer les chunks dont la source dominante était le silence avant de les expédier sur le socket — moins de bande passante, moins d'audio inutile à traiter pour le backend, et cela honorait un contrat client explicite du backend qui disait « n'envoie pas de silence ». Toutes les raisons de le faire étaient solides.
Et cela a complètement cassé la finalisation des utterances. Le commit qui le corrige expose la chaîne causale avec une candeur inhabituelle :
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.
C'est la forme platonicienne d'une abstraction qui fuit et vous mord en retour. Le détecteur d'activité vocale du transcripteur n'a pas d'entrée séparée « le locuteur a fini » — il déduit la fin d'une phrase en entendant environ une seconde de silence dans l'audio. Le silence n'était pas du bruit sur le fil ; c'était un message. En le filtrant pour économiser de la bande passante, le client a accidentellement cessé d'envoyer le seul signal qui disait au transcripteur qu'une phrase s'était terminée. Alors chaque transcription partielle ne faisait que grandir, la queue s'estompant en un ... qui ne se résolvait jamais.
Vous pourriez corriger cela en remettant le silence — mais alors vous avez défait l'optimisation. Le vrai correctif est meilleur : envoyer le sens du silence sans envoyer le silence. Le client suit désormais localement depuis combien de temps l'audio est silencieux, et quand cela franchit SILENCE_COMMIT_THRESHOLD_MS = 1000 — délibérément aligné sur le propre seuil de silence du VAD backend — il déclenche un signal commit explicite hors bande : sendMixedChunk remarque le silence prolongé, envoie un realtime-audio:commit par IPC, le processus principal transmet un { type: 'commit' } sur le socket, et le backend dit à ElevenLabs de finaliser le partiel. Deux petits morceaux d'état, silenceStartedAt et silenceCommitFired, l'empêchent de se déclencher à répétition. Le gain de bande passante survit ; la finalisation refonctionne ; le ... suspendu se résout.
Il y a aussi une petite note de bas de page honnête à cette version : la logique de reconnexion sans borne que la v1.9.0 a introduite a d'abord été construite dans le renderer sous forme de WsReconnectController, puis supprimée au sein de la même version une fois que la vraie implémentation a atterri dans le handler du processus principal, là où elle avait sa place. L'histoire de la reconnexion elle-même — backoff sans borne, fournisseurs alternés tous les cinq échecs — est racontée dans son propre chapitre ; ce que la v1.9.0 y ajoute, c'est qu'elle est là où cette architecture est née, et où le client en a pris possession pour la première fois.
Trois choses que ces versions nous ont apprises
- Quand vous ne pouvez pas empêcher la perte, rendez-la récupérable et rendez-la visible. La v1.8.20 n'empêche pas les reconnexions de jeter de l'audio — ce n'est pas physiquement possible en temps réel. Elle garde une copie de l'audio brut pour plus tard, et elle étiquette le trou dans la transcription. Ni l'une ni l'autre n'est glorieuse ; ensemble, elles transforment une perte de données silencieuse en une condition connue, bornée et récupérable.
- Un filet de sécurité a besoin de ses propres bornes. Le tee durable remplirait le disque en silence si on le laissait faire — un correctif qui devient un bug au ralenti. Le nettoyage à 7 jours livré dans la même version n'est pas une pensée après coup ; c'est ce qui rend le tee sûr à livrer tout court. Si vous ajoutez un mécanisme qui accumule, ajoutez dans la même foulée le mécanisme qui l'élague.
- Ce que vous jetez pour être efficace est peut-être un signal. La leçon la plus tranchante de l'une ou l'autre version : filtrer le silence était correct sur chaque axe que vous vérifieriez normalement — bande passante, charge, conformité au contrat — et cela a quand même cassé le produit, parce qu'un système en aval lisait un sens dans le silence. Quand vous retirez des données par efficacité, demandez-vous ce qui déduisait quelque chose de ces données. Parfois vous devez envoyer le sens explicitement précisément parce que vous avez cessé d'envoyer la chose dont il était auparavant déduit.
Et c'est la fin de l'histoire de la version 1 — le dernier durcissement avant la réécriture. Pour le chapitre précédent, les trois verbes qui gardent le Web Audio en vie (v1.8.16 + v2.0.2) ; pour là où tout cela se dirigeait, ce qu'exige vraiment une version 2 : 206 commits (v2.0.0) ; et pour tout l'arc, l'anatomie de livrer un logiciel jusqu'à la perfection.