J'ai annulé 2 724 lignes après qu'un changement audio IA ait cassé la production
J'ai poussé ce qui aurait dû être une amélioration du système vocal de ma plateforme podcast. Six jours et plusieurs commits plus tard, j'ai supprimé 2 724 lignes de code et fait un rollback vers ce qui fonctionnait. Voici ce qui s'est passé et ce que j'ai appris sur les tests de changements IA en production.
Je voulais écrire ce post plus tôt. Mais tout a pris plus de temps que prévu, et je voulais être sûr d'avoir l'histoire correcte avant de la partager.
Le 28 avril 2026, j'ai déployé ce que je pensais être une amélioration du système de text-to-speech de DIALØGUE. Un modèle vocal différent semblait prometteur sur le papier. J'avais entendu dire -- et constaté par moi-même -- que les voix du modèle actuel dériveraient sur les podcasts plus longs. Alors j'ai fait le changement.
Au 30 avril, j'avais supprimé 2 724 lignes de code et fait un rollback vers la version stable du 22 avril.
Le podcast d'un utilisateur réel était bloqué. Impossible de générer de l'audio. Le système l'avait marqué AUDIO_FAILED puis avait renvoyé une 409 Conflict lors de la relance, ce qui ressemblait au départ au vrai bug. Il s'avérait que c'était un symptôme -- le podcast était déjà dans un état d'échec au moment du retry. Le vrai problème était plus simple et plus grave : un segment avait fait un timeout sur la nouvelle configuration. Le fallback aussi. Et le système a tout simplement abandonné.
Pour aggraver les choses, il s'agissait d'un script qui se dirigeait vers 65 minutes d'audio parlé. Le podcast médian sur la plateforme dure 26 minutes. J'ai accepté un script plus de deux fois plus volumineux dans un système qui n'avait jamais généré avec succès quoi que ce soit d'aussi long. C'était ça, le vrai risque -- et le changement de modèle est ce qui l'a révélé.
Je dois l'admettre, ce n'était pas mon meilleur moment en tant que développeur. J'ai poussé un changement de configuration sans le type de stress testing qu'un système de production mérite. Voici ce qui s'est passé, ce qui s'est cassé, et ce que j'ai appris.
Ce que fait DIALØGUE
Si vous ne connaissez pas, DIALØGUE est un générateur de podcasts IA. Vous lui donnez un sujet, un PDF ou un épisode d'émission, et il produit un podcast conversationnel à deux animateurs. Le tout fonctionne sur la synthèse text-to-speech Gemini de Google.
Le pipeline audio fonctionne comme ceci :
- Générer un outline à partir du matériel source
- Développer l'outline en un script complet
- Diviser le script en segments
- Synthétiser l'audio pour chaque segment avec Gemini TTS
- Assembler les segments, normaliser le volume et uploader la MP3 finale
Le modèle par défaut en production était un modèle Gemini TTS qui fonctionnait bien pour la plupart des cas. Mais plus le script était long, plus on entendait la voix dériver entre les segments. L'Animateur A dans le segment un ne sonnait pas exactement comme l'Animateur A dans le segment six. Pour un podcast de 25 minutes, c'est perceptible mais on s'y habitue. Pour quelque chose qui approche les 40 minutes, ça devient vraiment rugueux.
J'ai décidé d'essayer une configuration de modèle différente. L'idée était d'améliorer la cohérence vocale sur les podcasts plus longs.
Ce que j'ai construit
Environ une semaine fin avril, j'ai poussé une série de commits pour tester une configuration de modèle différente. Ce n'était pas un changement anodin d'une ligne. J'ai construit un vrai changement d'architecture :
- Bascule du modèle TTS par défaut vers la nouvelle configuration
- Ajout d'une chaîne de fallback -- si le modèle principal faisait un timeout, fallback vers le modèle stable de production
- Construction d'un système de QA au niveau des chunks : découper les transcriptions en unités plus petites, synthétiser chacune, valider la qualité audio avec une analyse ffmpeg
- Ajout d'un suivi de progression du workflow pour que l'UI puisse afficher l'état de synthèse par segment
- Renforcement de la logique de retry -- trois tentatives par chunk, backoff exponentiel
- Ajout d'un gate de qualité audio longue qui bloquerait
COMPLETEsauf si la MP3 finale assemblée passait le QA audio
L'idée était de passer de "générer de l'audio par segment et espérer que ça sonne cohérent" à "chunker les transcriptions, QA de chaque chunk, relancer les échecs, valider le fichier final."
Au 28 avril, le code était déployé en production. Les tests unitaires passaient. Les tests d'intégration passaient. Ce que je n'avais pas -- et que j'aurais dû avoir -- c'était un load test lançant une exportation complète de 50+ minutes sur la nouvelle configuration. J'ai sauté le seul test qui comptait vraiment.
Ce qui s'est cassé
Presque immédiatement, un podcast en production a échoué.
C'était un long -- une analyse du capex de la Big Tech qui se dirigeait vers 65 minutes d'audio parlé. Le système est arrivé à l'étape audio et a renvoyé AUDIO_FAILED.
Au début, j'ai vu la 409 Conflict et j'ai pensé que l'orchestrator avait un bug. Il s'est avéré que la 409 était un symptôme secondaire -- une tentative de retry après que le podcast était déjà marqué comme échoué. Le premier échec était beaucoup plus simple.
Le segment 0 a fait un timeout sur la nouvelle configuration du modèle. Le timeout était fixé à 60 secondes. Le modèle fallback -- qui était le même modèle que celui utilisé par le chemin stable -- a aussi fait un timeout. Le système a marqué le podcast comme échoué et est passé à autre chose.
Le fallback aurait dû fonctionner. Mais le fallback était invoqué avec le même budget de timeout de 60 secondes, sur le même grand segment, dans le même contexte de request qui avait déjà consommé du temps lors de l'appel principal échoué. Le chemin stable, en revanche, appelle ce même modèle à neuf avec le timeout complet disponible et traite les segments individuellement. Même modèle, différentes conditions -- c'est pour ça que la version stable s'en sort bien mais pas le fallback.
J'ai désactivé le système de chunk QA en production avec un changement d'une ligne. Ça n'a pas corrigé le timeout. La nouvelle configuration continuait de faire des timeouts sur des segments que le modèle stable gérait sans problème.
Bon, c'est le genre de moment où il faut choisir : continuer à debugger le nouveau chemin, ou revenir à ce qui fonctionne.
Le rollback
Le 30 avril, j'ai pris la décision.
Le commit de rollback (4a5bfc8) a supprimé 2 724 lignes de code :
- Le module TTS chunker entier
- Les gates d'analyse de qualité audio
- Le suivi de progression du workflow
- Les test suites de retry de chunk QA
- La configuration de la chaîne de modèles fallback
Et a ajouté 321 lignes pour préserver les parties qui comptaient -- les formulations de prompt vocal localisées, la compatibilité frontend et la couverture de régression pour le chemin stable.
C'est beaucoup de lignes à supprimer. Pour être honnête, ça ressemblait à admettre une défaite. Mais non. C'était choisir le bon chemin à long terme plutôt que quelque chose qui ressemblait à du progrès mais qui n'était pas prêt.
Après le rollback, j'ai :
- Reconstruit l'image Docker de base partagée
- Redéployé le service
generate-speech - Réinitialisé le podcast échoué vers l'approbation du script
- Cliqué sur "Generate Audio" via l'UI normale
Le résultat :
- Modèle : Gemini TTS (configuration stable de production)
- Segments : 6 sur 6 terminés
- Durée : 1 527 secondes -- environ 25 minutes
- MP3 finale : 30,5 Mo
- Statut :
COMPLETE
Le podcast qui était bloqué depuis deux jours s'est terminé en environ 11 minutes après le rollback. L'épisode de l'utilisateur a été livré -- deux jours plus tard que prévu, mais complet.
Un détail sur le chiffre de 65 minutes : c'était la durée estimée basée sur la longueur du script brut, pas sur le résultat final. Après le reset, le script est passé par le pipeline de raccourcissement normal, et l'audio final assemblé est sorti à environ 25 minutes. Le script original aurait été encore plus long -- ce qui fait partie des raisons pour lesquelles il échouait.
Les données
Avant de décider quoi faire ensuite, je voulais des chiffres réels. Pas des impressions -- des données. J'ai interrogé la base de données de production pour les informations de durée des podcasts.
| Cohorte | Nombre | Durée médiane | p90 | Max |
|---|---|---|---|---|
| 30 derniers jours | 8 | 26 min | 31 min | 34 min |
| 90 derniers jours | 72 | 27 min | 36 min | 45 min |
| Toujours | 120 | 26 min | 30 min | 45 min |
Donc le podcast médian dure environ 26 minutes. Le p90 est autour de 34-36 minutes. Le podcast terminé le plus long jamais enregistré était de 45 minutes.
Et voici les données plus dures -- les podcasts qui ont atteint l'étape audio, classés par durée estimée :
| Durée estimée | A atteint l'audio | Terminés | Échoués | En cours |
|---|---|---|---|---|
| Moins de 15 min | 18 | 17 | 0 | 1 |
| 15-30 min | 36 | 31 | 2 | 3 |
| 30-40 min | 24 | 23 | 0 | 1 |
| 40-50 min | 3 | 1 | 2 | 0 |
| 50+ min | 2 | 0 | 2 | 0 |
« En cours » couvre les podcasts qui ont été lancés mais jamais terminés -- abandonnés par l'utilisateur, annulés pendant la génération, ou laissés dans un statut pending. Deux podcasts tentés à 50+ minutes. Les deux ont échoué à l'étape audio. Zéro terminé.
Aucun podcast en production n'a jamais atteint 50 minutes. Le système fonctionne bien pour la plage de 15-40 minutes. Au-dessus de 40 minutes, le risque augmente fortement. Et au-dessus de 50 minutes, c'est essentiellement un territoire inexploré.
Ce que j'ai appris
1. Les changements de configuration nécessitent un stress testing au niveau de la production
Je dois être honnête là-dessus. La nouvelle configuration du modèle aurait pu très bien fonctionner avec des prompts de test courts. Mais en production, avec des scripts réels et des contraintes de timeout réelles, elle a échoué sur des segments que la configuration stable gère sans problème.
Je n'ai pas fait de benchmark correct avant le déploiement. Un prompt de test court va réussir. Un podcast de 65 minutes avec plusieurs segments sous un timeout de 60 secondes, c'est un contexte complètement différent. L'écart entre ces deux scénarios, c'est là que vivent les incidents de production.
J'ai peut-être tort sur la configuration elle-même -- peut-être qu'elle avait juste besoin de plus de réglage ou d'un timeout plus long. Mais le point tient : j'ai changé une dépendance centrale de production sans le type de stress testing qu'un système sur lequel les gens comptent vraiment mérite.
2. L'incohérence vocale est le vrai problème, pas les timeouts
Le timeout était la panne visible. Mais la raison pour laquelle j'essayais une configuration différente en premier lieu, c'était l'incohérence vocale. Même sur le modèle stable, les voix changent entre les appels de synthèse. L'Animateur A dans le segment un ne sonne pas exactement comme l'Animateur A dans le segment six.
Pour les podcasts courts, c'est à peine perceptible. Pour les plus longs, ça s'accumule. Et pour les podcasts de 50+ minutes -- qui, encore une fois, n'ont jamais été terminés en production -- ce serait probablement vraiment flagrant.
L'approche par chunks essayait de résoudre ça en rendant chaque chunk plus petit et plus contrôlé. C'est la bonne direction, je pense. L'implémentation n'était juste pas encore prête pour la production.
3. J'avais besoin de telemetrie avant l'incident, pas après
Quand la panne s'est produite, je ne pouvais pas mapper le coût ou la performance TTS à des IDs de podcast spécifiques. Les logs n'avaient pas d'entrées utiles. Les événements de workflow n'avaient aucun enregistrement de génération TTS.
J'ai dû diagnostiquer la panne à partir du champ de statut du podcast, une erreur 409 confuse, et la reproduction locale du comportement de timeout. Ça a fini par fonctionner, mais ce n'était pas le genre d'expérience de debugging que je veux revivre.
J'ai ajouté de la telemetrie de coût TTS après -- modèle utilisé, modèle fallback, nombre de retries, statut par tentative, caractères de transcription, octets audio de sortie, durée audio. Ça aurait dû exister avant l'incident. De mon expérience, c'est toujours le cas. On construit l'observabilité après l'incendie, pas avant.
4. Faire un rollback n'est pas un échec
Supprimer 2 724 lignes de code, ça fait mal. Je ne vais pas prétendre le contraire. On passe une semaine à construire quelque chose, on est fier de l'architecture, et puis on démolit tout parce que ce n'est pas prêt.
Mais c'était la bonne décision. Le système de chunk QA était un bon design. Il reviendra -- comme un changement plus petit et isolé avec une validation canary appropriée. Juste pas dans le cadre d'un changement de configuration de modèle. Et pas tant que la nouvelle configuration continue de faire des timeouts sur des segments que le chemin stable gère sans problème.
5. L'épisode de 50 minutes est un produit différent
Celui-là m'a surpris. Je partais du principe que le système gérerait n'importe quelle longueur de script. Les données disent le contraire.
Si quelqu'un veut vraiment un podcast de 50 minutes, c'est un profil de génération différent. Il faut probablement un script qui est réduit à 45 minutes ou moins avant la génération audio. Un gate de révision manuel avant le TTS. Des budgets de timeout par segment plus solides. Peut-être même une stratégie de synthèse complètement différente.
Optimiser le chemin par défaut pour la plage de 15-40 minutes est la bonne décision. L'épisode de 50+ minutes devrait être traité comme un chemin exceptionnel, pas la norme. Je pense que c'est une décision produit, pas seulement une décision d'ingénierie.
Où en sont les choses maintenant
La production est de retour sur la configuration TTS stable. Ce n'est pas parfait. L'incohérence vocale sur les podcasts plus longs est toujours là, et je l'entends. Mais c'est suffisamment stable pour que la plateforme fonctionne dans la grande majorité des cas.
Si vous voulez écouter ce que DIALOGUE produit, vous pouvez essayer vous-même sur https://podcast.chandlernguyen.com.
Le rapport d'incident est commit dans le repository. Le rollback est documenté avec la forme SQL exacte pour réinitialiser un podcast échoué. La telemetrie est en place maintenant pour la prochaine tentative.
Et la leçon est plus claire que je ne l'aurais souhaité :
En IA de production, le coût de changer des dépendances centrales sans stress testing approprié n'est pas une expérience ratée. C'est une expérience utilisateur ratée.
Je réessaierai avec différentes configurations de modèle. Mais la prochaine fois, avec une meilleure telemetrie, un chemin de déploiement canary, et un stress test qui lance une exportation complète de 50+ minutes avant que quoi que ce soit ne touche la production.
Avant de changer des dépendances IA en production, voici la checklist que j'aurais dû suivre :
- Stress tester le pire scénario. Exécuter le workload le plus long et le plus lourd que vous anticipez -- pas juste un prompt de test court. Si votre système gère des podcasts de 40 minutes, testez avec un de 50 minutes.
- Logger la telemetrie par tentative dès le premier jour. Nom du modèle, modèle fallback, nombre de retries, timeout, caractères de transcription, octets audio de sortie, durée audio. Ça aurait dû exister avant l'incident. Ça l'est toujours.
- Canary sur un vrai job. Avant de basculer pour tout le monde, faites tourner un seul vrai job de production end to end sur la nouvelle config et vérifiez le résultat.
- Vérifier le fallback sous le même budget de timeout. Un fallback qui partage le même timeout que le principal n'est pas un fallback -- c'est une deuxième chance d'échouer dans les mêmes conditions.
- Définir le SQL de rollback et le runbook avant de déployer. Savoir exactement comment réinitialiser un job bloqué. Documenter les commandes. Si vous n'avez pas ça, vous allez improviser pendant l'incident.
Si vous construisez des systèmes IA en production, quel est votre pattern pour tester les changements de configuration de modèle sans casser l'expérience des utilisateurs ? J'aimerais vraiment savoir. Avez-vous déjà vécu un moment similaire de "j'ai changé un truc et tout s'est cassé" ?
Bien à vous,
Chandler





