
On a supprimé 5,000 lignes de code audio — et les transcriptions ont commencé à s'afficher en double
GeekBye v1.6 a arraché deux transcripteurs Swift embarqués et les a remplacés par un pipeline unifié — une suppression nette de plus de 5,000 lignes, réalisée pendant que des utilisateurs étaient en pleine réunion. Puis les transcriptions ont commencé à apparaître en double, car le bug s'était déplacé vers la seule couche qui croyait encore qu'il y avait deux moteurs.
Il existe une terreur particulière à remanier le code qui tourne en ce moment même, pour des gens en pleine réunion. Impossible de mettre la fonctionnalité en maintenance — quelqu'un s'appuie dessus pour retranscrire une conversation qu'on ne peut pas rejouer. GeekBye v1.6 a fait exactement ça sur le chemin le plus critique de l'app : le pipeline audio. Voici l'histoire de ce qu'on a supprimé, ce qui a cassé, et les deux leçons de fiabilité qui en sont sorties.
Deux moteurs, dont un en Swift
Avant v1.6, la reconnaissance vocale de GeekBye tournait en embarqué dans des binaires Swift. Il y en avait deux : l'un encapsulant le framework Speech d'Apple, et un plus grand (plus de 1,500 lignes) qui capturait l'audio dual — votre micro plus l'audio système — via ScreenCaptureKit et ouvrait son propre WebSocket directement vers notre backend pour streamer vers Deepgram. Deux transcripteurs, deux chemins de capture, deux façons de parler au réseau, le tout dans un langage compilé où chaque changement imposait de recompiler un binaire.
v1.6.0 a tout condensé en un seul pipeline. Le commit unifiant a supprimé le transcripteur Apple Speech (~1,145 lignes), le captureur Swift de Deepgram (~1,523 lignes), les deux bridges TypeScript, l'ancien orchestrateur et les handlers IPC dual-audio — une suppression nette de plus de 5,200 lignes. À leur place : le renderer capture l'audio via des Web APIs standard, l'envoie en PCM par IPC, et le processus principal d'Electron possède un seul chemin WebSocket vers le backend, avec le fournisseur choisi par la config du backend. Un chemin de capture, un propriétaire de socket, un seul endroit pour raisonner.
Supprimer cinq mille lignes, ça fait du bien. Pendant environ une journée.
Le bug s'est déplacé vers la couche qui croyait encore qu'il y en avait deux
Puis les transcriptions ont commencé à s'afficher en double. Une seule phrase prononcée était sauvegardée en deux entrées identiques — parfois pire.
La cause racine est la chose la plus instructive de toute cette version, parce que c'est une loi générale de l'unification. On avait unifié les fournisseurs dans le processus principal — un seul socket, un seul handler. Mais la couche React n'avait pas reçu le mémo : elle montait encore deux hooks d'événements spécifiques aux fournisseurs simultanément, un conçu pour Deepgram et un pour ElevenLabs. Les deux écoutaient. Les deux sauvegardaient. Chaque transcription était persistée par tous les hooks en vie — et ils l'étaient tous les deux.
Et il y avait un second doublement, plus sournois, en dessous : ElevenLabs émet chaque ligne finalisée deux fois — une fois comme événement committed et une fois comme committed_with_timestamps. Donc même avec un seul hook, une phrase pouvait arriver en deux événements.
Le correctif (dans la ligne v1.6) est un diptyque bien rangé qui correspond exactement aux deux causes : conditionner chaque hook d'événement derrière un flag enabled piloté par la config du fournisseur actif du backend, pour qu'un seul listener soit jamais en vie ; et ajouter une déduplication de texte par source (mémoriser la dernière ligne sauvegardée pour le micro et pour l'audio système) pour avaler la double émission d'ElevenLabs.
La leçon est plus grande que ce bug : quand on unifie deux backends derrière une interface, la duplication ne disparaît pas — elle migre vers la couche qui suppose encore qu'il y en a deux. On a condensé la tuyauterie dans le processus principal et la fuite a resurgi deux couches plus haut, dans le câblage des événements de l'interface. Unifier un pipeline n'est pas terminé quand le producteur est unifié ; c'est terminé quand chaque consommateur a arrêté de croire en l'ancienne forme.
La première reconnexion : « abandonner après cinq tentatives »
v1.6.0 a aussi livré la toute première reconnexion automatique WebSocket avec backoff exponentiel de GeekBye — et ça vaut la peine de la montrer précisément parce que c'est l'ancêtre primitif de la version à toute épreuve qu'on utilise aujourd'hui.
L'original était borné. Cinq tentatives, délais de 1s, 2s, 4s, 8s, 16s — puis il capitulait, émettant une erreur fatale : « Connexion perdue — veuillez redémarrer la transcription. » À l'époque, ça semblait responsable : ne pas relancer indéfiniment, dire honnêtement à l'utilisateur. En pratique, c'était un bug UX déguisé. Un incident réseau de seize secondes — un basculement Wi-Fi, une reconnexion VPN, un tunnel — est passager, mais une relance bornée le traite comme terminal. L'utilisateur n'avait rien fait de mal et on lui demandait de redémarrer une réunion en cours.
C'est exactement l'échec que la conception actuelle a éliminé. Aujourd'hui GeekBye se reconnecte avec un backoff non borné et change même de fournisseur, et ne s'arrête que pour une raison vraiment fatale — auth, quota, facturation. Tout le parcours de « abandonner après cinq » vers « ne jamais abandonner sur une erreur passagère » est raconté dans pourquoi votre notetaker IA s'arrête sur un mauvais Wi-Fi. v1.6, c'est là que cette route a commencé, avec la version naïve qui devait exister en premier.
Les connexions fantômes vous hantent sans nom
Une dernière leçon de fiabilité a atterri dans v1.6.3, corrigeant une classe subtile de bugs que quiconque fait de la reconnexion sur un backend à état rencontrera inévitablement.
Quand on se reconnecte — ou qu'on change la langue de transcription en cours de session, ce qui provoque une reconnexion en coulisses — l'ancienne connexion ne meurt pas toujours silencieusement. Ses messages d'agonie (CONNECTION_LOST, disconnected) peuvent arriver après que la nouvelle connexion est déjà saine, et détruire le remplacement parfaitement fonctionnel. Le correctif donne à chaque tentative de connexion une identité — un ID de session par connexion estampillé dans l'URL du WebSocket — plus une courte fenêtre de grâce après reconnexion pendant laquelle les messages de déconnexion obsolètes d'une tentative précédente sont ignorés. Il envoie aussi un message stop explicite avant de fermer un socket pour que le backend puisse démonter proprement l'ancienne session.
Le principe : toute logique de reconnexion sur un backend à état doit tagger chaque tentative avec une identité et traiter les messages tardifs des tentatives précédentes comme du bruit — sinon une reconnexion réussie se fait tuer par le dernier souffle de son prédécesseur.
Trois choses qu'a enseignées v1.6
- L'unification déplace les bugs ; elle ne les supprime pas. Condensez deux moteurs en un et auditez chaque consommateur qui croit encore qu'il y en a deux. Le nôtre, c'était l'interface, qui montait deux hooks d'événements après que la tuyauterie avait déjà fusionné.
- Votre première reconnexion sera bornée, et borné est faux pour les streams en direct. « Abandonner après N tentatives » transforme un incident passager en panne terminale dont on tient l'utilisateur responsable. Distinguez le fatal (auth, quota) du passager (un socket coupé) dès la toute première version.
- Les reconnexions ont besoin d'identité. Taguez chaque tentative ; ignorez les messages tardifs des tentatives mortes. Une connexion sans nom peut être tuée par son propre fantôme.
C'est le deuxième chapitre de l'histoire de fiabilité qui devient GeekBye v2. Pour le premier, voir l'épopée de la mise à jour automatique qui a pris six versions en quatre jours (v1.5.x) ; pour là où le travail de reconnexion a finalement abouti, pourquoi votre notetaker IA s'arrête sur un mauvais Wi-Fi ; et pour l'arc complet, l'anatomie de livrer un logiciel à la perfection.