Ich habe 2.724 Zeilen Code zurückgenommen, nachdem eine KI-Audio-Änderung die Produktion zerstört hat
Ich wollte ein Upgrade für das Voice-System meiner Podcast-Plattform einspielen. Sechs Tage und mehrere Commits später löschte ich 2.724 Zeilen Code und machte einen Rollback auf das, was funktioniert hatte. Hier ist, was passiert ist und was ich über das Testen von KI-Änderungen in der Produktion gelernt habe.
Ich wollte diesen Beitrag eigentlich früher schreiben. Aber die ganze Angelegenheit hat länger gebraucht, als ich dachte, und ich wollte sicherstellen, dass die Geschichte stimmt, bevor ich sie teile.
Am 28. April 2026 habe ich etwas deployed, von dem ich dachte, es sei eine Verbesserung für DIALØGUEs Text-to-Speech-System. Ein anderes Voice-Modell sah auf dem Papier vielversprechend aus. Ich hatte gehört -- und selbst gemerkt -- dass die Stimmen des aktuellen Modells bei längeren Podcasts drifteten. Also habe ich gewechselt.
Bis zum 30. April hatte ich 2.724 Zeilen Code gelöscht und einen Rollback auf den stabilen Stand vom 22. April gemacht.
Ein echter Podcast eines Nutzers hing fest. Er konnte kein Audio generieren. Das System hatte ihn als AUDIO_FAILED markiert und dann bei einem Retry einen 409 Conflict geworfen, was zuerst wie der eigentliche Bug aussah. Es war aber nur ein Symptom -- der Podcast war bereits in einem fehlgeschlagenen Zustand, als der Retry-Versuch stattfand. Das eigentliche Problem war simpler und schlimmer: Ein Segment lief unter der neuen Konfiguration in einen Timeout. Der Fallback ebenfalls. Und das System hat dann einfach aufgegeben.
Was die Sache noch schlimmer machte: Es ging um ein Skript, das auf etwa 65 Minuten gesprochenes Audio zusteuerte. Der Median-Podcast auf der Plattform liegt bei 26 Minuten. Ich habe ein Skript, das mehr als doppelt so lang war, durch ein System geschickt, das noch nie annähernd so viel erfolgreich generiert hatte. Das war das eigentliche Risiko -- und die Modelländerung hat es erst sichtbar gemacht.
Ich muss zugeben, das war nicht meine beste Stunde als Entwickler. Ich habe eine Konfigurationsänderung eingespielt, ohne das Stress-Testing, das ein Produktionssystem verdient. Hier ist, was passiert ist, was kaputtgegangen ist und was ich daraus gelernt habe.
Was DIALØGUE macht
Falls ihr es noch nicht kennt: DIALØGUE ist ein KI-Podcast-Generator. Man gibt ihm ein Thema, ein PDF oder eine Show-Episode, und es produziert einen conversational Podcast mit zwei Moderatoren. Das Ganze läuft auf Googles Gemini Text-to-Speech-Synthese.
Die Audio-Pipeline funktioniert so:
- Outline aus dem Quellmaterial generieren
- Outline zu einem vollständigen Skript ausarbeiten
- Skript in Segmente aufteilen
- Audio für jedes Segment mit Gemini TTS synthetisieren
- Segmente zusammenfügen, Lautstärke normalisieren und finale MP3 hochladen
Das Produktions-Standardmodell war ein Gemini TTS-Modell, das für die meisten Fälle gut funktionierte. Aber je länger das Skript, desto hörbarer driftete die Stimme zwischen den Segmenten auseinander. Moderator A im ersten Segment klang nicht exakt wie Moderator A im sechsten. Bei einem 25-minütigen Podcast ist es hörbar, aber man gewöhnt sich dran. Ab 40 Minuten wird es richtig schwierig.
Ich wollte eine andere Modellkonfiguration ausprobieren. Die Idee war, die Stimm-Konsistenz bei längeren Podcasts zu verbessern.
Was ich gebaut habe
Über etwa eine Woche Ende April habe ich eine Reihe von Commits eingespielt, um eine andere Modellkonfiguration zu testen. Das war keine einfache Einzeilen-Änderung. Ich habe eine ganze Architekturumstellung gebaut:
- Standard-TTS-Modell auf die neue Konfiguration umgestellt
- Fallback-Kette hinzugefügt -- wenn das primäre Modell in einen Timeout läuft, Fallback auf das stabile Produktionsmodell
- Chunk-Level-QA-System gebaut: Transkripte in kleinere Einheiten aufteilen, jede einzeln synthetisieren, Audio-Qualität mit ffmpeg-Analyse validieren
- Workflow-Fortschritts-Tracking für die UI, um den Synthese-Status pro Segment anzuzeigen
- Retry-Logik verstärkt -- drei Versuche pro Chunk, exponentielles Backoff
- Long-Form-Audio-Qualitäts-Gate, das
COMPLETEblockiert, solange die finale MP3 die Audio-QA nicht besteht
Die Idee war der Wechsel von "Audio pro Segment generieren und hoffen, dass es konsistent klingt" hin zu "Transkripte chunken, jeden Chunk QA-en, Fehlschläge retryen, die finale Datei validieren."
Bis zum 28. April war der Code in der Produktion deployed. Die Unit-Tests waren grün. Die Integrationstests waren grün. Was ich nicht hatte -- und hätte haben sollen -- war ein Lasttest, der einen vollständigen 50+-Minuten-Export gegen die neue Konfiguration laufen lässt. Ich habe den einen Test ausgelassen, der tatsächlich wichtig war.
Was kaputtgegangen ist
Fast sofort ist ein Produktions-Podcast fehlgeschlagen.
Es war ein langer -- eine Big-Tech-Capex-Analyse, die auf etwa 65 Minuten gesprochenes Audio zusteuerte. Das System erreichte den Audio-Schritt und lieferte AUDIO_FAILED zurück.
Zuerst sah ich den 409 Conflict und dachte, der Orchestrator habe einen Bug. Der 409 war aber ein sekundäres Symptom -- ein Retry-Versuch, nachdem der Podcast bereits als fehlgeschlagen markiert war. Der erste Ausfall war viel simpler.
Segment 0 lief unter der neuen Modellkonfiguration in einen Timeout. Der Timeout war auf 60 Sekunden eingestellt. Das Fallback-Modell -- das gleiche Modell, das der stabile Pfad nutzt -- lief ebenfalls in einen Timeout. Das System markierte den Podcast als fehlgeschlagen und machte weiter.
Der Fallback hätte funktionieren sollen. Aber der Fallback wurde mit demselben 60-Sekunden-Timeout-Budget aufgerufen, auf demselben großen Segment, im selben Request-Kontext, der bereits Zeit durch den fehlgeschlagenen Primary-Aufruf verbraucht hatte. Der stabile Pfad dagegen ruft dasselbe Modell frisch auf, mit dem vollen verfügbaren Timeout, und verarbeitet Segmente einzeln. Gleiches Modell, andere Bedingungen -- deshalb kann die stabile Version es problemlos handhaben, der Fallback aber nicht.
Ich habe das Chunk-QA-System in der Produktion mit einer einzeiligen Änderung deaktiviert. Das hat den Timeout nicht behoben. Die neue Konfiguration lief weiterhin in Timeouts bei Segmenten, die das stabile Modell problemlos verarbeitet.
Nun, das ist einer dieser Momente, in denen man sich entscheiden muss: Weiter den neuen Pfad debuggen oder zurück zu dem, was funktioniert.
Der Rollback
Am 30. April habe ich die Entscheidung getroffen.
Der Rollback-Commit (4a5bfc8) hat 2.724 Zeilen Code gelöscht:
- Das gesamte TTS-Chunker-Modul
- Audio-Qualitätsanalyse-Gates
- Workflow-Fortschritts-Tracking
- Die Chunk-QA-Retry-Test-Suites
- Die Fallback-Modellketten-Konfiguration
Und 321 Zeilen hinzugefügt, um die Teile zu erhalten, die wichtig waren -- lokalisierte Voice-Prompt-Formulierungen, Frontend-Kompatibilität und Regression-Coverage für den stabilen Pfad.
Das sind viele Zeilen, die man löscht. Es fühlte sich ehrlich gesagt wie eine Niederlage an. Aber es war keine. Es war die Entscheidung für den richtigen langfristigen Weg statt für etwas, das nach Fortschritt aussah, aber noch nicht bereit war.
Nach dem Rollback habe ich:
- Das shared Base-Docker-Image neu gebaut
- Den
generate-speech-Service neu deployed - Den fehlgeschlagenen Podcast zurück auf Skript-Freigabe gesetzt
- über die normale UI auf "Generate Audio" geklickt
Das Ergebnis:
- Modell: Gemini TTS (stabile Produktionskonfiguration)
- Segmente: 6 von 6 abgeschlossen
- Dauer: 1.527 Sekunden -- etwa 25 Minuten
- Finale MP3: 30,5 MB
- Status:
COMPLETE
Der Podcast, der zwei Tage lang festhing, war nach dem Rollback in etwa 11 Minuten fertig. Die Episode des Nutzers wurde ausgeliefert -- zwei Tage später als geplant, aber vollständig.
Eine Anmerkung zur 65-Minuten-Zahl: Das war die geschätzte Dauer basierend auf der Roh-Skriptlänge, nicht die finale Ausgabe. Nach dem Reset durchlief das Skript die normale Verkürzungs-Pipeline, und das finale Audio kam auf etwa 25 Minuten. Das ursprüngliche Skript wäre noch länger gewesen -- was auch ein Teil des Grunds war, warum es fehlschlug.
Die Daten
Bevor ich entschied, was als Nächstes zu tun war, wollte ich echte Zahlen. Nicht Gefühle -- Daten. Ich habe die Produktionsdatenbank nach Podcast-Dauerinformationen abgefragt.
| Kohorte | Anzahl | Median-Dauer | p90 | Max |
|---|---|---|---|---|
| Letzte 30 Tage | 8 | 26 Min. | 31 Min. | 34 Min. |
| Letzte 90 Tage | 72 | 27 Min. | 36 Min. | 45 Min. |
| Insgesamt | 120 | 26 Min. | 30 Min. | 45 Min. |
Der Median-Podcast liegt also bei etwa 26 Minuten. Das p90 bei rund 34-36 Minuten. Der längste abgeschlossene Podcast aller Zeiten war 45 Minuten.
Und hier sind die härteren Daten -- Podcasts, die die Audio-Phase erreicht haben, aufgeschlüsselt nach geschätzter Dauer:
| Geschätzte Dauer | Audio erreicht | Abgeschlossen | Fehlgeschlagen | In Bearbeitung |
|---|---|---|---|---|
| Unter 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 |
"In Bearbeitung" umfasst Podcasts, die gestartet, aber nie abgeschlossen wurden -- vom Nutzer aufgegeben, während der Generierung abgebrochen oder im Pending-Status liegen geblieben. Zwei Podcast-Versuche bei 50+ Minuten. Beide in der Audio-Phase fehlgeschlagen. Keiner abgeschlossen.
Kein Produktions-Podcast hat je 50 Minuten erreicht. Das System funktioniert gut für den 15-40-Minuten-Bereich. Ab 40 Minuten steigt das Risiko stark an. Und ab 50 Minuten ist es im Grunde ungetestetes Terrain.
Was ich gelernt habe
1. Konfigurationsänderungen brauchen produktionsreifes Stress-Testing
Ich muss hier ehrlich sein. Die neue Modellkonfiguration hätte bei kurzen Test-Prompts gut funktionieren können. Aber in der Produktion, mit echten Skripten und echten Timeout-Grenzen, ist sie bei Segmenten fehlgeschlagen, die die stabile Konfiguration problemlos verarbeitet.
Ich habe vor dem Deployment nicht richtig gebenchmarkt. Ein kurzer Test-Prompt wird durchgehen. Ein 65-Minuten-Podcast mit mehreren Segmenten unter 60-Sekunden-Timeout ist ein völlig anderer Kontext. Die Lücke zwischen diesen beiden Szenarien ist dort, wo Produktionsvorfälle wohnen.
Ich mag mich in der Konfiguration selbst täuschen -- vielleicht brauchte sie nur mehr Feinjustierung oder einen längeren Timeout. Aber der Punkt bleibt: Ich habe eine zentrale Produktions-Abhängigkeit geändert, ohne das Stress-Testing, das ein System verdient, auf das sich Menschen tatsächlich verlassen.
2. Stimm-Inkonsistenz ist das eigentliche Problem, nicht Timeouts
Der Timeout war der sichtbare Ausfall. Aber der Grund, warum ich überhaupt eine andere Konfiguration ausprobiert habe, war Stimm-Inkonsistenz. Selbst beim stabilen Modell verschieben sich Stimmen zwischen Synthese-Aufrufen. Moderator A im ersten Segment klingt nicht exakt wie Moderator A im sechsten.
Bei kurzen Podcasts ist das kaum hörbar. Bei längeren summiert es sich. Und bei 50+-Minuten-Podcasts -- die, noch mal, niemand in der Produktion abgeschlossen hat -- wäre es wahrscheinlich richtig offensichtlich.
Der Chunked-Ansatz versuchte, das zu lösen, indem jeder Chunk kleiner und kontrollierter wurde. Das ist die richtige Richtung, denke ich. Die Implementierung war nur noch nicht ready für die Produktion.
3. Ich brauchte Telemetrie vor dem Vorfall, nicht danach
Als der Ausfall passierte, konnte ich TTS-Kosten oder -Performance nicht bestimmten Podcast-IDs zuordnen. Die Logs hatten keine brauchbaren Einträge. Die Workflow-Events hatten keine TTS-Generierungs-Daten.
Ich musste den Ausfall über den Status-Field des Podcasts, einen verwirrenden 409-Fehler und lokale Reproduktion des Timeout-Verhaltens diagnostizieren. Es hat letztendlich funktioniert, aber das war nicht die Art von Debugging-Erfahrung, die ich noch mal haben will.
Ich habe danach TTS-Kosten-Telemetrie hinzugefügt -- verwendetes Modell, Fallback-Modell, Retry-Anzahl, Status pro Versuch, Transkript-Zeichen, Output-Audio-Bytes, Audio-Dauer. Das hätte schon vor dem Vorfall existieren sollen. Aus meiner Erfahrung ist das immer so. Man baut die Observability nach dem Feuer, nicht davor.
4. Rollback ist kein Scheitern
2.724 Zeilen Code zu löschen fühlte sich schlecht an. Ich werde nicht so tun, als ob nicht. Man verbringt eine Woche mit Bauen, ist stolz auf die Architektur, und dann reißt man alles runter, weil es nicht bereit ist.
Aber es war die richtige Entscheidung. Das Chunk-QA-System war gutes Design. Es wird zurückkommen -- als kleinere, isolierte Änderung mit proper Canary-Validierung. Nur nicht als Teil einer Modellkonfigurationsänderung. Und nicht solange das neue Setup noch bei Segmenten in Timeouts läuft, die der stabile Pfad problemlos handhabt.
5. Die 50-Minuten-Episode ist ein anderes Produkt
Das hat mich überrascht. Ich dachte, das System würde jede Skriptlänge verarbeiten. Die Daten sagen etwas anderes.
Wenn jemand wirklich einen 50-Minuten-Podcast will, ist das ein anderes Generationsprofil. Es braucht wahrscheinlich ein Skript, das vor der Audio-Generierung auf 45 Minuten oder weniger gekürzt wird. Einen manuellen Review-Gate vor dem TTS. Stärkere Timeout-Budgets pro Segment. Vielleicht sogar eine komplett andere Synthese-Strategie.
Den Standardpfad für den 15-40-Minuten-Bereich zu optimieren, ist die richtige Entscheidung. Die 50+-Minuten-Episode sollte als Ausnahmepfad behandelt werden, nicht als Normalfall. Ich denke, das ist eine Produkt-Entscheidung, nicht nur eine technische.
Wo die Dinge jetzt stehen
Die Produktion ist zurück auf der stabilen TTS-Konfiguration. Sie ist nicht perfekt. Die Stimm-Inkonsistenz bei längeren Podcasts ist immer noch da, und ich kann sie hören. Aber sie ist stabil genug, dass die Plattform für die allermeisten Anwendungsfälle funktioniert.
Wenn ihr hören wollt, was DIALOGUE produziert, könnt ihr es selbst ausprobieren unter https://podcast.chandlernguyen.com.
Der Incident Report ist im Repository committed. Der Rollback ist dokumentiert, mit der genauen SQL-Struktur für das Zurücksetzen eines fehlgeschlagenen Podcasts. Die Telemetrie ist jetzt da für den nächsten Versuch.
Und die Lehre ist klarer, als mir lieb wäre:
Bei Produktions-KI sind die Kosten, zentrale Abhängigkeiten ohne proper Stress-Testing zu ändern, nicht ein gescheitertes Experiment. Es ist eine gescheiterte Nutzererfahrung.
Ich werde es wieder mit anderen Modellkonfigurationen versuchen. Aber nächstes Mal mit besserer Telemetrie, einem Canary-Deployment-Pfad und einem Stress-Test, der einen vollständigen 50+-Minuten-Export laufen lässt, bevor irgendetwas die Produktion berührt.
Bevor man Produktions-KI-Abhängigkeiten ändert, hier ist die Checklist, der ich hätte folgen sollen:
- Den Worst-Case-Job stress-testen. Den längsten, schwersten Workload ausführen, den ihr erwartet -- nicht nur einen kurzen Test-Prompt. Wenn euer System 40-Minuten-Podcasts verarbeitet, testet mit einem 50-Minuten-Podcast.
- Telemetrie pro Versuch von Tag eins loggen. Modellname, Fallback-Modell, Retry-Anzahl, Timeout, Transkript-Zeichen, Output-Audio-Bytes, Audio-Dauer. Das hätte schon vor dem Vorfall existieren sollen. Tut es immer.
- Einen echten Job canary-en. Bevor ihr für alle umschaltet, lasst einen einzelnen echten Produktions-Job end-to-end auf der neuen Konfiguration laufen und prüft das Ergebnis.
- Fallback unter demselben Timeout-Budget verifizieren. Ein Fallback, der sich denselben Timeout mit dem Primary teilt, ist kein Fallback -- er ist eine zweite Chance, unter denselben Bedingungen zu scheitern.
- Rollback-SQL und Runbook vor dem Deployment definieren. Genau wissen, wie man einen steckengebliebenen Job zurücksetzt. Die Befehle dokumentieren. Wenn ihr das nicht habt, improvisiert ihr während des Incidents.
Wenn ihr Produktions-KI-Systeme baut: Was ist euer Pattern, um Modellkonfigurationsänderungen zu testen, ohne Nutzer zu beeinträchtigen? Ich würde das genuinely gern wissen. Hattet ihr auch schon mal diesen "eine Sache geändert und alles kaputt"-Moment?
Viele Grüße,
Chandler





