Par Juste Thales Gnimavo (CEO, ZeroSuite) et Claude Opus 5.5 (Claude Code)
Déblo Duo est une app d'interprétation en temps réel : chacun parle sa langue et entend l'autre dans la sienne. Pourquoi Déblo Duo existe présente le produit. Ce billet porte sur un de ses modes, le mains libres : un seul téléphone posé sur la table entre deux personnes, le micro toujours ouvert, personne n'appuie sur rien. Il couvre trois jours. Le 11 novembre 2026, un banc nous a dit ce qui n'allait pas dans le mode que nous avions. Le 12 novembre, nous avons découvert que ce banc mesurait une copie de la logique, pas le code qui part en production. Le 13 novembre, la version 1.1.6 est sortie sur Google Play, et les premiers appels mains libres du CEO étaient muets.
Pourquoi le mains libres est difficile
Avec deux téléphones, ou avec l'appui maintenu, l'app sait toujours qui parle. Avec un seul téléphone et un micro ouvert, elle ne le sait pas. Le même micro entend la personne A, la personne B, et le haut-parleur du téléphone qui joue la traduction. Pour chaque phrase, le système doit décider de quelle langue il s'agit, quel sens doit la traduire, et que faire du micro pendant que la traduction joue.
Notre mode mains libres de l'époque répondait simplement à la dernière question : couper le micro pendant la lecture d'une traduction, pour que le téléphone ne traduise pas sa propre voix. Cela paraissait raisonnable. Personne ne l'avait mesuré.
11 novembre : le banc
Claude a ouvert la session en rejouant le brief dans le code. Le banc existant rejouait une session dans un seul sens. Il ne savait pas reproduire un téléphone et deux langues. Claude en a donc écrit un nouveau : une conversation à deux langues rejouée sur un micro, deux sessions de traduction, une piste de sortie arbitrée comme l'app l'arbitre. Sept variantes, de l'appui maintenu (la référence) à plusieurs formes de mains libres, sur trois jeux de clips : conversation propre, phrases très courtes, et voix de fond dans une troisième langue. Langues de test : chinois, japonais, espagnol et arabe, puis français et anglais.
Aucun code de production n'a été modifié ce jour-là. Le banc ne faisait que mesurer.
La variante mains libres en place perdait de la parole sur 71 tours sur 72. La raison était mécanique. La traduction commence environ 3 secondes après le début de la phrase. À ce moment, le micro est coupé, puisqu'une traduction joue. La fin de la phrase n'atteint jamais le modèle. Quelqu'un qui annonce un prix en fin de phrase aurait entendu le prix disparaître.
Le second constat était plus discret. Le sens qui devait traduire détectait la bonne langue 215 fois sur 215. Le sens qui venait de traduire, non. Il restait collé à sa langue : 56 fausses détections sur 216, zéro au premier tour, 29 au cinquième. Plus la conversation durait, plus c'était grave.
Ouvrir une session neuve pour ce sens à chaque tour supprimait la dérive. Avec cette forme, sur conversation propre, le banc comptait 72 tours sans doublon ni mauvaise langue, un tour jamais entendu, et environ 3,1 secondes avant le premier son traduit. L'appui maintenu, la référence, restait plus rapide : environ 1,9 seconde.
Restait une décision produit, et elle revenait à Juste. Il a choisi ce qu'il a appelé le comportement « film » : « micro jamais coupé, traduction au fur et à mesure ». Il a fixé le prix qu'il acceptait de payer : « Jusqu'à 4 s avant le premier son traduit, acceptable. » Il a aussi demandé que l'app dise où le mode a sa place : le mains libres, c'est pour un endroit calme.
La session s'est arrêtée là. Le serveur et l'écran étaient laissés à une session neuve.
12 novembre : le banc testait une copie
La session suivante a commencé de la même façon, en rejouant le brief dans le code. Tous les symboles cités par le brief existaient dans le pont de traduction, le module qui tourne en production. Puis Claude a regardé ce que le banc importait.
Il n'importait pas le pont. Le banc avait sa propre implémentation de la logique. Les 72 tours sans faute du 11 novembre mesuraient la bonne forme de l'algorithme sur une copie, pas le code qui allait partir.
C'est un écart facile à manquer. La copie avait été écrite d'après la même conception, par le même agent, la veille. Il aurait été naturel de lire ses chiffres comme des chiffres sur la production. Ils ne l'étaient pas. Une copie peut être juste pendant que le vrai code est faux, et le banc n'avait aucun moyen de le voir.
La réponse a été structurelle. La logique mains libres est passée dans un petit module pur partagé par le worker et le banc, et une nouvelle variante du banc exécutait l'objet de direction du worker lui-même au lieu d'une réimplémentation.
Cela a payé dès le premier passage. La première écriture du changement serveur par Claude ajoutait une méthode à une dataclass, et cette méthode portait le même nom qu'un champ de la dataclass. En Python, cela remplace le champ sans rien dire. La liste de langues passée à la session de traduction contenait une fonction au lieu d'un code de langue. Aucune session ne s'ouvrait, dans les trois modes, pas seulement en mains libres. py_compile passait. Le fichier était syntaxiquement correct.
Le premier passage du banc sur le vrai code a échoué sur une erreur de validation, le worker abandonnant. La méthode a été renommée. Claude a écrit dans le log de session la phrase qui résume la journée : « Sans le banc sur le vrai code, ce défaut partait en prod. »
Après le renommage, le banc a joué 234 tours sur le code qui part : 72 en conversation propre, 18 en anglais, 72 avec une voix de fond 15 dB sous l'orateur, 72 où la même personne enchaîne deux fois. Aucune erreur de session, aucune reconnexion, aucun préchauffage échoué. En conversation propre : aucun doublon, aucune mauvaise langue, un tour jamais entendu, 3,5 secondes avant le premier son traduit, sous les 4 secondes acceptées par Juste. Avec la voix de fond : un doublon, un mauvais sens, cinq tours jamais entendus.
Le côté écran a été fait le même jour par deux sessions workers, l'une après l'autre, sur l'app mobile.
13 novembre : la 1.1.6, et un premier appel muet
La version 1.1.6 de Duo est passée en production sur Google Play le matin du 13 novembre. Côté App Store, le build 1.1.6 a été rattaché à la version et envoyé en examen par Juste. Le même matin, en reprenant les fiches des stores, nous avons constaté que la FAQ de l'app annonçait un essai gratuit de « 2 minutes » dans toutes les langues. La production en donne 10. La FAQ a été corrigée dans le code ; elle part avec le prochain build.
Puis Juste a passé ses premiers appels mains libres sur la nouvelle version. Silence, sur iPhone et sur Android.
Les journaux du worker le montraient. Sur Android, le mains libres était actif 4,2 secondes après le début de l'appel, l'audio est arrivé pendant 47 secondes, et pas un mot n'a été transcrit : du silence numérique. Sur iPhone, l'appel est resté muet jusqu'à ce que Juste en sorte et y revienne ; la traduction a alors démarré cinq secondes plus tard. Dans les deux journaux, l'événement « micro attaché » et l'événement « mains libres actif » étaient à 0,1 seconde d'écart.
C'était l'indice. Une course dans l'app. À la connexion, l'app publie le micro puis le coupe, parce que les appels démarrent micro coupé. Mais le statut du worker pouvait passer l'appel en ligne avant la fin de cette séquence. Le mains libres ouvrait alors le micro, et la coupure de démarrage passait ensuite et le refermait. Deux événements, à un dixième de seconde d'écart, dans le mauvais ordre.
Le vrai correctif est dans l'app : la coupure du micro attend désormais la fin de sa mise en place. Claude l'a écrit et vérifié au typage. Mais il demande un nouveau build, et les téléphones déjà en 1.1.6 seraient restés muets jusqu'à la mise à jour.
Alors, plutôt que de dépenser un build, Claude a ajouté un filet côté serveur, pour les téléphones déjà en 1.1.6. Si le mains libres est actif et que le worker n'entend rien au-dessus du silence venant de l'hôte pendant quelques secondes, il répond « off » ; l'app redemande et rouvre le micro. Deux relances au plus, et seulement avant la première trame vivante.
Cette dernière condition vient d'une erreur. La première version du filet se déclenchait à tort en pleine conversation. Les téléphones appliquent une annulation d'écho, et pendant qu'une traduction joue, l'annulation d'écho rend du silence sur le micro. Pour le filet, cela ressemblait à un micro mort. Il relançait au milieu d'un appel qui fonctionnait. Les journaux de 08 h 44 UTC sur iPhone et de 08 h 50 UTC sur Android l'ont montré. La règle a été restreinte au début de l'appel, avant toute trame vivante, et le worker reconstruit.
Ce qui est prouvé, et ce qui ne l'est pas
Sur les deux reconstructions du worker, le journal collé par Juste montrait cinq appels mains libres à froid, chacun le premier appel après l'ouverture de l'app : à 08 h 44 UTC sur iPhone et 08 h 50 UTC sur Android (les deux appels où la première version s'est ensuite déclenchée à tort), puis trois autres à 08 h 55 et 08 h 56. Cinq sur cinq traduits, la première parole reconnue 5 à 6 secondes après l'entrée en mains libres. Aucun appel muet. Le verdict de Juste : « tout marche ».
Nous ne disons pas corrigé, et la raison est dans le même log. Le filet ne s'est déclenché avant la première parole dans aucun de ces cinq appels. Il n'a jamais été vu réparer le cas pour lequel il a été construit. Les cinq appels prouvent que le mains libres a fonctionné cinq fois de suite, sur deux téléphones, ce matin-là. Ils ne prouvent pas pourquoi. Le correctif qui traite la cause est dans l'app, et il n'est pas livré.
Ce qui reste ouvert
- Phrases courtes en chinois, japonais, espagnol et arabe : non mesurées avec la forme finale. Un mot seul d'une seconde n'est transcrit dans aucun mode, appui maintenu compris.
- Bascule de session pendant que quelqu'un parle déjà : non mesurée.
- Charge par appel : deux sessions vivantes plus un préchauffage par appel mains libres. L'exposition aux erreurs
1011du service de traduction est à surveiller en production. - Voix de synthèse : tous les chiffres du banc viennent de parole synthétique, et le banc n'entend pas le haut-parleur du téléphone. L'écho du haut-parleur dans un micro ouvert, volume haut, ne se prouve que sur de vrais appareils.
- Le correctif côté app du premier appel muet est écrit et vérifié au typage. Il n'est pas livré.
La leçon
Un banc n'est honnête que dans la mesure de ce qu'il importe. Le 11 novembre, le nôtre a produit des chiffres propres et utiles, et ils ont guidé une vraie décision produit. C'étaient aussi des chiffres sur une copie. Rien dans les résultats ne le disait. La seule façon de le voir était de lire les lignes d'import du banc le lendemain matin, en rejouant le brief dans le code.
La règle que nous en tirons est courte. Un test qui n'exécute pas le module de production mesure une intention. Quand le banc a exécuté le vrai module, il a trouvé dès sa première minute un défaut que le compilateur avait accepté et qui n'aurait ouvert de session pour personne. Deux jours plus tard, la production a trouvé un défaut qu'aucun banc n'aurait pu trouver, parce qu'il tenait à l'ordre de deux événements sur un vrai téléphone. Les deux exigeaient la chose réelle. C'est aussi pour cela que l'appel muet reste ouvert : cinq bons appels sont une observation, pas une explication.