AI音声の変更にproductionが壊れて2,724行をロールバックした話
ポッドキャストプラットフォームの音声システムをアップグレードするつもりで変更をデプロイした。 6日と数コミット後、2,724行のコードを削除して安定版にロールバックした。 これが実際に起きたことと、productionのAI変更にテストをどう扱うべきかについて学んだことだ。
この投稿はもっと早く書くつもりだった。だが全体を整理するのに予想より時間がかかり、共有する前に話を正確に把握したかった。
2026年4月28日、DIALØGUEのtext-to-speechシステムの改善になると思ってデプロイした。別のvoice modelが紙面上では有望に見えた。現在のmodelの音声が長いポッドキャストで drift するのを — 自分でも聞いていて — 感じていた。そこで切り替えた。
4月30日までに、2,724行のコードを削除して4月22日の安定pathにロールバックした。
実際のユーザーのポッドキャストが動かなくなっていた。audioを生成できない。システムは AUDIO_FAILED をマークし、リトライ時に 409 Conflict をスローした。最初はこれがバグそのものだと思った。結果としてこれは症状にすぎなかった — リトライ发生时にはポッドキャストはすでにfailed状態だった。本当の問題はもっと単純で、もっと深刻だった。新しい設定でセグメントがtimeoutした。fallbackもtimeoutした。そしてシステムは諦めた。
さらに悪いことに、これは65分近い音声になるスクリプトだった。プラットフォームのポッドキャストの中央値は26分。その2倍以上のサイズを、それに近いものを一度も成功裡に生成したことのないシステムに通した。それが本当のリスクだった — そしてmodel変更がそれを露呈させた。
正直に言うと、ビルダーとしてベストな瞬間ではなかった。productionシステムが deserve する種類のstress testingなしに設定変更をpushした。何が起きたのか、何が壊れたのか、何を学んだのか。ここに残す。
DIALØGUEは何をするのか
見たことがない人のために言うと、DIALØGUEはAIポッドキャストジェネレーターだ。トピック、PDF、またはショーのエピソードを与えると、2人のホストによる会話形式のポッドキャストを生成する。すべてGoogleのGemini text-to-speech synthesisで動いている。
audio pipelineはこう動く:
- ソース資料からoutlineを生成
- outlineを完全なスクリプトに展開
- スクリプトをセグメントに分割
- Gemini TTSで各セグメントのaudioをsynthesize
- セグメントを結合し、loudnessをnormalizeし、最終的なMP3をアップロード
production defaultは、ほとんどのユースケースでうまく動くGemini TTS modelだった。だがスクリプトが長いほど、セグメント間でvoiceのdriftが聞こえやすくなる。セグメント1のホストAとセグメント6のホストAが完全に同じに聞こえない。25分のポッドキャストなら気づくが慣れる。40分に近づくとかなり厳しくなる。
別のmodel設定を試すことにした。狙いは長いポッドキャスト全体でのvoiceの一貫性改善だ。
何が作られたか
4月下旬の約1週間、別のmodel設定を試す一連のコミットをpushした。気軽な1行の変更ではなかった。アーキテクチャ全体を_shift_した:
- default TTS modelを新しい設定に切り替え
- fallback chainを追加 — primary modelがtimeoutしたら安定版production modelにfallback
- chunkレベルのQAシステムを構築:トランスクリプトを小さな単位に分割し、それぞれをsynthesizeし、ffmpeg analysisでaudio品質を検証
- UIがセグメントごとのsynthesis状態を表示できるようworkflow progress trackingを追加
- retryロジックを強化 — チャンクごとに3回試行、exponential backoff
- 最終的に組み立てられたMP3がaudio QAを通過しない限り
COMPLETEをブロックするlong-form audio quality gateを追加
狙いは「セグメントごとにaudioを生成して一貫性を願う」から「トランスクリプトをchunkし、各chunkをQAし、失敗をリトライし、最終ファイルを検証する」への移行だった。
4月28日までにコードはproductionにデプロイされた。ユニットテストはpassした。インテグレーションテストもpassした。持っていなかったもの — そして持つべきだったもの — は新しい設定に対してフルの50分以上のexportを実行するload testだ。実際に mattered テストをスキップした。
何が壊れたか
ほぼ即座に、productionのポッドキャストが失敗した。
長いやつだった — Big Tech capexの分析で、65分近い音声になる見込みだった。システムはaudioステップに到達して AUDIO_FAILED を返した。
最初は 409 Conflict を見て、orchestratorにバグがあると思った。結果として 409 は二次的な症状だった — ポッドキャストがすでにfailedとマークされた後のリトライ試行。最初の失敗はずっと単純だった。
セグメント0が新しいmodel設定でtimeoutした。timeoutは60秒に設定されていた。fallback model — 安定pathが使うのと同じmodel — もtimeoutした。システムはポッドキャストをfailedとしてマークして次に進んだ。
fallbackは動くはずだった。だがfallbackは、failedしたprimary callで already 時間を消費した同じリクエストコンテキスト内で、同じ60秒のtimeout budgetで、同じ大きなセグメントに対して呼び出された。一方、安定pathは同じmodelをフルのtimeout availableでfreshに呼び出し、セグメントを個別に処理する。同じmodel、異なる条件 — だから安定版は問題なく処理できるがfallbackはできなかった。
productionでchunk QAシステムを1行の変更で無効化した。timeoutは直らなかった。新しい設定は、安定modelが問題なく処理するセグメントで依然としてtimeoutしていた。
さて、こういう瞬間には選ばないといけない。新しいpathのデバッグを続けるか、動くものに戻るか。
ロールバック
4月30日、決断した。
ロールバックコミット(4a5bfc8)は 2,724行 のコードを削除した:
- TTS chunkerモジュール全体
- audio品質分析gate
- workflow progress tracking
- chunk QAリトライテストスイート
- fallback model chain設定
そして321行を追加して重要な部分 — localized voice promptの表現、frontend互換性、安定pathのためのregressionカバレッジ — を保持した。
削除するにはかなりの行数だ。率直に言って、敗北を認めるような気分だった。だがそうではない。進歩に見えたが準備ができていないものより、長期的に正しいpathを選んだのだ。
ロールバック後、以下を行った:
- shared base Dockerイメージを再ビルド
generate-speechサービスを再デプロイ- failedしたポッドキャストをスクリプト承認にリセット
- 通常のUIから「Generate Audio」をクリック
結果:
- Model: Gemini TTS(安定版production設定)
- セグメント: 6件中6件完了
- 再生時間: 1,527秒 — 約25分
- 最終MP3: 30.5 MB
- ステータス:
COMPLETE
2日間動かなかったポッドキャストが、ロールバック後約11分で完了した。ユーザーのエピソードは出荷された — 予定より2日遅れだが、完成した。
65分という数字について一つ:これは生のスクリプト長に基づく推定値で、最終的なoutputではない。リセット後、スクリプトは通常の短縮pipelineを通り、最終的に組み立てられたaudioは約25分になった。元のスクリプトはさらに長かったはずだ — それが失敗した理由の一部でもある。
データ
次に何をするか決める前に、実際の数字が欲しかった。感覚じゃない、データだ。production databaseにポッドキャストの再生時間情報をクエリした。
| コホート | 数 | 中央値 | p90 | 最大 |
|---|---|---|---|---|
| 過去30日 | 8 | 26分 | 31分 | 34分 |
| 過去90日 | 72 | 27分 | 36分 | 45分 |
| 全期間 | 120 | 26分 | 30分 | 45分 |
ポッドキャストの中央値は約26分。p90は34〜36分くらいだ。過去最長の完了ポッドキャストは45分。
そしてさらに厳しいデータ — audioステージに到達したポッドキャストを推定再生時間別に分類したもの:
| 推定再生時間 | audioに到達 | 完了 | 失敗 | 進行中 |
|---|---|---|---|---|
| 15分未満 | 18 | 17 | 0 | 1 |
| 15〜30分 | 36 | 31 | 2 | 3 |
| 30〜40分 | 24 | 23 | 0 | 1 |
| 40〜50分 | 3 | 1 | 2 | 0 |
| 50分以上 | 2 | 0 | 2 | 0 |
「進行中」は開始されたが完了しなかったポッドキャストをカバーする — ユーザーによる放棄、生成中のキャンセル、pending状態の放置。50分以上で試行されたポッドキャストは2件。両方ともaudioステージで失敗。完了はゼロ。
50分に達した完了済みのproductionポッドキャストは一件もない。システムは15〜40分の範囲ではうまく動いている。40分を超えるとリスクは急上昇する。そして50分以上は、実質的に未検証の領域だ。
学んだこと
1. 設定変更にはproductionレベルのstress testingが必要
これについては正直にならなければいけない。新しいmodel設定は短いテストプロンプトではうまく動いたかもしれない。だがproductionでは、本物のスクリプトと本物のtimeout制約の下で、安定設定が問題なく処理するセグメントで失敗した。
デプロイ前にまともにbenchmarkしなかった。短いテストプロンプトは成功する。60秒のtimeoutで複数のセグメントを持つ65分のポッドキャストはまったく異なるコンテキストだ。この2つのシナリオのギャップ — そこにproductionインシデントは棲んでいる。
設定自体については間違っているかもしれない — もう少しのtuningや長いtimeoutが必要だっただけかもしれない。だがポイントは変わらない。人々が実際に依存するシステムが deserve する種類のstress testingなしにcore production dependencyを変更したということだ。
2. 真の問題はtimeoutではなくvoiceの不整合だ
timeoutは目に見える失敗だった。だが最初に別の設定を試そうとした理由はvoiceの不整合だった。安定modelでさえ、音声がsynthesisコール間でshiftする。セグメント1のホストAはセグメント6のホストAと完全に同じに聞こえない。
短いポッドキャストでは barely 気づくレベルだ。長いものでは蓄積する。そして50分以上のポッドキャスト — 繰り返しになるが、productionで完了した人はいない — ではおそらく非常に顕著だっただろう。
chunkedアプローチは各chunkを小さくコントロールすることでこれを解決しようとしていた。それは正しい方向だと思う。ただ実装がまだproductionの準備できていなかっただけだ。
3. インシデント前にtelemetryが必要だった、後ではない
失敗が起きたとき、TTSコストやパフォーマンスを特定のポッドキャストIDにマップできなかった。ログに有用なエントリがなかった。workflowイベントにTTS生成レコードがなかった。
ポッドキャストのstatusフィールド、紛らわしい 409 エラー、そしてローカルでのtimeout動作の再現から失敗を診断する必要があった。最終的にはうまくいったが、もう一度経験したい種類のデバッグ体験ではなかった。
その後TTSコストtelemetryを追加した — 使用model、fallback model、リトライ回数、試行ごとのステータス、トランスクリプト文字数、output audioバイト数、audio再生時間。これはインシデント前に存在すべきものだった。経験から言うと、これはいつもそうだ。観測性は火事の後に建てるもので、前ではない。
4. ロールバックは失敗ではない
2,724行のコードを削除するのは気分が悪かった。そうじゃないふりはしない。1週間かけて何かをビルドし、アーキテクチャに誇りを持ち、そして準備ができていないからすべてを取り壊す。
だが正しい判断だった。chunk QAシステムは良いデザインだった。戻ってくる — 小さく隔離された変更として、properなcanary validationとともに。ただmodel設定変更の一部としてではない。そして新しい設定が安定pathで問題なく処理するセグメントで依然としてtimeoutしている間はなおさらだ。
5. 50分のエピソードは別のプロダクトだ
これは意外だった。システムはどんな長さのスクリプトでも処理できると思っていた。データはそうではないと言う。
誰かが本当に50分のポッドキャストを望むなら、それは別のgeneration profileだ。おそらくaudio生成前に45分以下にtightenされるスクリプトが必要になる。TTS前のmanual review gate。セグメントごとのより強いtimeout budget。あるいはまったく別のsynthesis戦略でさえ。
default pathを15〜40分の範囲に最適化するのが正しい判断だ。50分以上のエピソードはnormではなくexceptional pathとして扱うべきだ。これはengineeringというよりproductの判断だと思う。
現状
productionは安定版TTS設定に戻った。完璧ではない。長いポッドキャストでのvoice不整合はまだあり、自分でも聞こえる。だがプラットフォームが大多数のユースケースで機能するくらいには安定している。
DIALOGUEが何を生成するか聞いてみたいなら、https://podcast.chandlernguyen.com で自分で試せる。
インシデントレポートはリポジトリにコミット済み。ロールバックはfailedポッドキャストをリセットするための正確なSQL shapeとともにドキュメント化されている。telemetryは今、次の試行のために用意されている。
そして教訓は自分が望んでいた以上に明確だ:
production AIにおいて、properなstress testingなしにcore dependenciesを変更するコストは失敗した実験ではない。失敗したユーザーエクスペリエンスだ。
別のmodel設定で再挑戦する。だが次は、より良いtelemetryと、canary deployment pathと、何かがproductionに触れる前にフルの50分以上のexportを実行するstress testとともに。
production AI dependenciesを変更する前に、従うべきだったチェックリスト:
- 最悪ケースのjobをstress testする。 期待する最長・最重量のワークロードを実行する — 短いテストプロンプトだけではない。システムが40分のポッドキャストを処理できるなら、50分でテストする。
- 初日から試行ごとのtelemetryをログに残す。 model名、fallback model、リトライ回数、timeout、トランスクリプト文字数、output audioバイト数、audio再生時間。インシデント前に存在すべきもの。いつもそうだ。
- 実際のjobを1つcanaryする。 みんなのためにスイッチをflipする前に、実際のproduction jobを1つ新しい設定でend to endに実行し、outputを検証する。
- 同じtimeout budget下でfallbackを検証する。 primaryと同じtimeoutを共有するfallbackはfallbackではない — 同じ条件下で失敗する2回目のチャンスだ。
- デプロイ前にrollback SQLとrunbookを定義する。 止まったjobをリセットする方法を正確に知っておく。コマンドをドキュメント化する。これがなければ、インシデント中に即興で対応することになる。
production AIシステムをビルドしている皆さん。ユーザーを壊さずにmodel設定変更をテストするパターンはありますか?純粋に知りたい。何かを変えたら全部壊れた、というような経験はありますか?
Chandler





