Hice rollback de 2.724 líneas después de que un cambio en el audio de IA rompiera producción
Hice un cambio que debería haber sido una mejora al sistema de voz de mi plataforma de podcast. Seis días y varios commits después, eliminé 2.724 líneas de código y regresé a lo que funcionaba. Esto es lo que pasó y lo que aprendí sobre probar cambios de IA en producción.
Tenía la intención de escribir este post antes. Pero todo tomó más tiempo del que esperaba, y quería asegurarme de tener la historia correcta antes de compartirla.
El 28 de abril de 2026, despliegué lo que pensé era una mejora al sistema de text-to-speech de DIALØGUE. Un modelo de voz diferente parecía prometedor en el papel. Había escuchado -- y comprobado por mí mismo -- que las voces del modelo actual se desviaban en podcasts más largos. Así que hice el cambio.
Para el 30 de abril, había eliminado 2.724 líneas de código y hecho rollback al camino estable del 22 de abril.
El podcast de un usuario real estaba trabado. No podía generar audio. El sistema lo había marcado como AUDIO_FAILED y luego lanzó un 409 Conflict al reintentar, lo que al principio parecía el bug real. Resultó ser un síntoma -- el podcast ya estaba en un estado fallido cuando ocurrió el retry. El problema real era más simple y peor: un segmento hizo timeout en la nueva configuración. El fallback también hizo timeout. Y el sistema simplemente se rindió.
Para empeorar las cosas, se trataba de un guión que se dirigía hacia 65 minutos de audio hablado. El podcast mediano en la plataforma dura 26 minutos. Acepté un guión más del doble de ese tamaño en un sistema que nunca había generado exitosamente nada parecido. Ese era el riesgo real -- y el cambio de modelo fue lo que lo expuso.
Tengo que admitirlo, este no fue mi mejor momento como constructor. Hice un cambio de configuración sin el tipo de stress testing que un sistema de producción merece. Esto es lo que pasó, qué se rompió y qué aprendí.
Qué hace DIALØGUE
Si no lo conocen, DIALØGUE es un generador de podcasts con IA. Le das un tema, un PDF o un episodio de programa, y produce un podcast conversacional de dos presentadores. Todo funciona con la síntesis text-to-speech Gemini de Google.
El pipeline de audio funciona así:
- Generar un outline a partir del material fuente
- Expandir el outline en un guión completo
- Dividir el guión en segmentos
- Sintetizar audio para cada segmento usando Gemini TTS
- Unir los segmentos, normalizar el volumen y subir la MP3 final
El default de producción era un modelo Gemini TTS que funcionaba bien para la mayoría de los casos. Pero cuanto más largo el guión, más se notaba cómo la voz se desviaba entre segmentos. El Presentador A en el segmento uno no sonaba exactamente igual que el Presentador A en el segmento seis. Para un podcast de 25 minutos, se nota pero te acostumbras. Para algo que se acerca a los 40 minutos, se pone difícil.
Decidí probar una configuración de modelo diferente. La idea era mejorar la consistencia de voz en podcasts más largos.
Lo que construí
Durante aproximadamente una semana a finales de abril, hice una serie de commits para probar una configuración de modelo diferente. Esto no fue un cambio casual de una línea. Construí un cambio de arquitectura completo:
- Cambié el modelo TTS default a la nueva configuración
- Agregué una cadena de fallback -- si el modelo principal hacía timeout, fallback al modelo estable de producción
- Construí un sistema de QA a nivel de chunk: dividir transcripciones en unidades más pequeñas, sintetizar cada una, validar la calidad de audio con análisis de ffmpeg
- Agregué tracking de progreso del workflow para que la UI pudiera mostrar el estado de síntesis por segmento
- Reforcé la lógica de retry -- tres intentos por chunk, backoff exponencial
- Agregué un gate de calidad de audio largo que bloquearía
COMPLETEa menos que la MP3 final ensamblada pasara el QA de audio
La idea era pasar de "generar audio por segmento y esperar que suene consistente" a "chunk de transcripciones, QA de cada chunk, reintentar fallos, validar el archivo final."
Para el 28 de abril, el código estaba desplegado en producción. Los tests unitarios pasaron. Los tests de integración pasaron. Lo que no tenía -- y debería haber tenido -- era un load test corriendo una exportación completa de 50+ minutos contra la nueva configuración. Me salté el único test que realmente importaba.
Qué se rompió
Casi inmediatamente, un podcast de producción falló.
Era uno largo -- un análisis de capex de Big Tech que se dirigía hacia 65 minutos de audio hablado. El sistema llegó al paso de audio y devolvió AUDIO_FAILED.
Al principio, vi el 409 Conflict y pensé que el orchestrator tenía un bug. Resultó que el 409 era un síntoma secundario -- un intento de retry después de que el podcast ya estaba marcado como fallido. El primer fallo fue mucho más simple.
El segmento 0 hizo timeout en la nueva configuración del modelo. El timeout estaba configurado a 60 segundos. El modelo fallback -- que era el mismo modelo que usa el camino estable -- también hizo timeout. El sistema marcó el podcast como fallido y continuó.
El fallback debería haber funcionado. Pero el fallback fue invocado con el mismo presupuesto de timeout de 60 segundos, en el mismo segmento grande, en el mismo contexto de request que ya había consumido tiempo en la llamada primaria fallida. El camino estable, en cambio, llama a ese mismo modelo en limpio con todo el timeout disponible y procesa segmentos individualmente. Mismo modelo, diferentes condiciones -- por eso la versión estable lo maneja bien pero el fallback no.
Desactivé el sistema de chunk QA en producción con un cambio de una línea. Eso no arregló el timeout. La nueva configuración seguía haciendo timeout en segmentos que el modelo estable manejaba sin problemas.
Bueno, este es el tipo de momento en el que hay que elegir: seguir debuggeando el nuevo camino, o volver a lo que funciona.
El rollback
El 30 de abril, tomé la decisión.
El commit de rollback (4a5bfc8) eliminó 2.724 líneas de código:
- Todo el módulo TTS chunker
- Gates de análisis de calidad de audio
- Tracking de progreso del workflow
- Los test suites de retry de chunk QA
- La configuración de cadena de modelo fallback
Y agregó 321 líneas para preservar las partes que importaban -- frases de prompt de voz localizadas, compatibilidad con el frontend y coverage de regresión para el camino estable.
Son muchas líneas para eliminar. Siendo honesto, se sintió como admitir una derrota. Pero no lo era. Era elegir el camino correcto a largo plazo sobre algo que parecía progreso pero no estaba listo.
Después del rollback:
- Reconstruí la imagen Docker base compartida
- Redesplegué el servicio
generate-speech - Resetié el podcast fallido de vuelta a aprobación de guión
- Hice clic en "Generate Audio" a través de la UI normal
El resultado:
- Modelo: Gemini TTS (configuración estable de producción)
- Segmentos: 6 de 6 completados
- Duración: 1.527 segundos -- unos 25 minutos
- MP3 final: 30,5 MB
- Estado:
COMPLETE
El podcast que había estado trabado durante dos días terminó en unos 11 minutos después del rollback. El episodio del usuario se envió -- dos días después de lo planeado, pero completo.
Una cosa sobre el número de 65 minutos: esa era la duración estimada basada en la longitud del guión crudo, no en el output final. Después del reset, el guión pasó por el pipeline de acortamiento normal, y el audio final ensamblado resultó en unos 25 minutos. El guión original habría sido aún más largo -- lo cual es parte de por qué estaba fallando.
Los datos
Antes de decidir qué hacer después, quería números reales. No sensaciones -- datos. Consulté la base de datos de producción para obtener información sobre la duración de los podcasts.
| Cohorte | Cantidad | Duración Mediana | p90 | Máx |
|---|---|---|---|---|
| Últimos 30 días | 8 | 26 min | 31 min | 34 min |
| Últimos 90 días | 72 | 27 min | 36 min | 45 min |
| Todo el tiempo | 120 | 26 min | 30 min | 45 min |
Así que el podcast mediano dura unos 26 minutos. El p90 ronda los 34-36 minutos. El podcast completado más largo de todos los tiempos fue de 45 minutos.
Y aquí están los datos más duros -- podcasts que llegaron a la etapa de audio, desglosados por duración estimada:
| Duración Estimada | Llegó a Audio | Completados | Fallidos | En Progreso |
|---|---|---|---|---|
| Menos 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 Progreso" cubre podcasts que se iniciaron pero nunca se completaron -- abandonados por el usuario, cancelados durante la generación, o dejados en estado pendiente. Dos podcasts intentados a 50+ minutos. Ambos fallaron en la etapa de audio. Cero completados.
Ningún podcast de producción completado ha alcanzado nunca los 50 minutos. El sistema funciona bien para el rango de 15-40 minutos. Por encima de 40 minutos, el riesgo sube bruscamente. Y por encima de 50 minutos, es esencialmente territorio sin probar.
Lo que aprendí
1. Los cambios de configuración necesitan stress testing a nivel de producción
Tengo que ser honesto con esto. La nueva configuración del modelo podría haber funcionado bien en prompts de prueba cortos. Pero en producción, con guiones reales y restricciones de timeout reales, falló en segmentos que la configuración estable maneja sin problema.
No hice un benchmark adecuado antes de desplegar. Un prompt de prueba corto va a funcionar. Un podcast de 65 minutos con múltiples segmentos bajo un timeout de 60 segundos es un contexto completamente diferente. La brecha entre esos dos escenarios es donde viven los incidentes de producción.
Puedo estar equivocado sobre la configuración en sí -- tal vez solo necesitaba más ajuste o un timeout más largo. Pero el punto se mantiene: cambié una dependencia central de producción sin el tipo de stress testing que merece un sistema en el que la gente realmente confía.
2. La inconsistencia de voz es el problema real, no los timeouts
El timeout fue el fallo visible. Pero la razón por la que estaba probando una configuración diferente en primer lugar era la inconsistencia de voz. Incluso en el modelo estable, las voces cambian entre llamadas de síntesis. El Presentador A en el segmento uno no suena exactamente igual que el Presentador A en el segmento seis.
Para podcasts cortos, esto apenas se nota. Para los más largos, se acumula. Y para podcasts de 50+ minutos -- que, de nuevo, nadie ha completado en producción -- probablemente sería realmente obvio.
El enfoque de chunks intentaba resolver esto haciendo cada chunk más pequeño y controlado. Esa es la dirección correcta, creo. La implementación simplemente no estaba lista para producción todavía.
3. Necesitaba telemetría antes del incidente, no después
Cuando ocurrió el fallo, no podía mapear el costo o rendimiento de TTS a IDs de podcast específicos. Los logs no tenían entradas útiles. Los eventos del workflow no tenían registros de generación TTS.
Tuve que diagnosticar el fallo desde el campo de estado del podcast, un error 409 confuso, y la reproducción local del comportamiento de timeout. Funcionó al final, pero no fue el tipo de experiencia de debugging que quiero volver a tener.
Agregué telemetría de costo de TTS después -- modelo usado, modelo fallback, cantidad de retries, estado por intento, caracteres de transcripción, bytes de audio de salida, duración del audio. Esto debería haber existido antes del incidente. Por mi experiencia, siempre es así. Construyes la observabilidad después del incendio, no antes.
4. Hacer rollback no es fracasar
Eliminar 2.724 líneas de código se sintió mal. No voy a fingir lo contrario. Pasas una semana construyendo algo, estás orgulloso de la arquitectura, y luego lo demueles todo porque no está listo.
Pero fue la decisión correcta. El sistema de chunk QA era un buen diseño. Va a volver -- como un cambio más pequeño y aislado con validación canary apropiada. Solo que no como parte de un cambio de configuración de modelo. Y no mientras la nueva configuración siga haciendo timeout en segmentos que el camino estable maneja sin problemas.
5. El episodio de 50 minutos es un producto diferente
Este me sorprendió. Asumí que el sistema manejaría cualquier longitud de guión. Los datos dicen lo contrario.
Si alguien genuinamente quiere un podcast de 50 minutos, ese es un perfil de generación diferente. Probablemente necesita un guión que se reduzca a 45 minutos o menos antes de la generación de audio. Un gate de revisión manual antes del TTS. Presupuestos de timeout por segmento más fuertes. Tal vez incluso una estrategia de síntesis completamente diferente.
Optimizar el camino default para el rango de 15-40 minutos es la decisión correcta. El episodio de 50+ minutos debería tratarse como un camino excepcional, no como la norma. Creo que esa es una decisión de producto, no solo de ingeniería.
Dónde están las cosas ahora
Producción está de vuelta en la configuración estable de TTS. No es perfecta. La inconsistencia de voz en podcasts más largos sigue ahí, y puedo escucharla. Pero es lo suficientemente estable como para que la plataforma funcione en la gran mayoría de los casos.
Si quieren escuchar lo que produce DIALOGUE, pueden probarlo ustedes mismos en https://podcast.chandlernguyen.com.
El reporte del incidente está en el repositorio. El rollback está documentado con la forma exacta del SQL para resetear un podcast fallido. La telemetría ya está en su lugar para el próximo intento.
Y la lección es más clara de lo que me hubiera gustado:
En IA de producción, el costo de cambiar dependencias centrales sin el stress testing adecuado no es un experimento fallido. Es una experiencia de usuario fallida.
Voy a intentarlo de nuevo con diferentes configuraciones de modelo. Pero la próxima vez, con mejor telemetría, un camino de deployment canary, y un stress test que corra una exportación completa de 50+ minutos antes de que algo toque producción.
Antes de cambiar dependencias de IA en producción, esta es la checklist que debería haber seguido:
- Stress test del peor caso. Ejecutar el workload más largo y pesado que esperen -- no solo un prompt de prueba corto. Si su sistema maneja podcasts de 40 minutos, prueben con uno de 50.
- Log de telemetría por intento desde el día uno. Nombre del modelo, modelo fallback, cantidad de retries, timeout, caracteres de transcripción, bytes de audio de salida, duración del audio. Esto debería haber existido antes del incidente. Siempre es así.
- Canary en un trabajo real. Antes de activar para todos, ejecuten un solo trabajo real de producción end to end en la nueva configuración y verifiquen el resultado.
- Verificar el fallback con el mismo presupuesto de timeout. Un fallback que comparte el mismo timeout que el primario no es un fallback -- es una segunda oportunidad de fallar bajo las mismas condiciones.
- Definir el SQL de rollback y el runbook antes de desplegar. Saber exactamente cómo resetear un trabajo trabado. Documentar los comandos. Si no tienen esto, van a improvisar durante el incidente.
Si construyen sistemas de IA de producción, ¿cuál es su patrón para probar cambios de configuración de modelo sin afectar a los usuarios? Genuinamente me gustaría saber. ¿Han tenido un momento similar de "cambié una cosa y todo se rompió"?
Saludos,
Chandler





