मैंने 2,724 lines rollback कर दीं जब एक AI audio change ने production तोड़ दिया
मैंने अपने podcast platform के voice system में एक upgrade push किया। छह दिन और कई commits बाद, मैंने 2,724 lines of code delete कर दीं और उस stable version पर rollback कर दिया जो काम कर रहा था। यहाँ बताया गया है कि क्या हुआ और production में AI changes test करने के बारे में मैंने क्या सीखा।
मैं इस post को जल्दी लिखना चाहता था। लेकिन पूरी चीज़ को सुलझाने में मुझे उम्मीद से ज़्यादा वक़्त लगा, और मैं share करने से पहले यह सुनिश्चित करना चाहता था कि कहानी सही हो।
28 अप्रैल, 2026 को, मैंने DIALØGUE के text-to-speech system में क्या एक improvement था, उसे deploy किया। एक अलग voice model काग़ज़ पर promising लग रहा था। मैंने सुना था -- और खुद भी अनुभव किया था -- कि current model की voices लंबे podcasts पर drift कर जाती हैं। तो मैंने switch कर दिया।
30 अप्रैल तक, मैंने 2,724 lines of code delete कर दी थीं और 22 अप्रैल के stable path पर rollback कर दिया था।
एक real user का podcast अटक गया था। वह audio generate नहीं कर पा रहा था। System ने उसे AUDIO_FAILED mark कर दिया था और फिर retry पर 409 Conflict throw किया, जो पहले actual bug लग रहा था। पता चला कि यह एक symptom था -- podcast पहले से ही failed state में था जब retry हुआ। असली problem ज़्यादा simple और ज़्यादा बुरा था: एक segment ने new configuration पर timeout कर दिया। Fallback ने भी timeout कर दिया। और फिर system ने बस हार मान ली।
और बुरी बात तो यह थी कि यह script लगभग 65 मिनट की spoken audio की तरफ बढ़ रहा था। Platform पर median podcast 26 मिनट का है। मैंने एक script जो उस size से दोगुना से भी ज़्यादा था, ऐसे system से गुज़ारा जिसने कभी इतने करीब कुछ सफलतापूर्वक generate नहीं किया था। यही असली risk था -- और model change ने उसे expose किया।
मुझे admit करना होगा, यह एक builder के तौर पर मेरा best moment नहीं था। मैंने एक configuration change push कर दिया बिना उस stress testing के जिसका एक production system हक़दार है। यहाँ बताया गया है कि क्या हुआ, क्या टूटा, और मैंने क्या सीखा।
DIALØGUE क्या करता है
अगर आपने इसे नहीं देखा है, तो DIALØGUE एक AI podcast generator है। आप इसे एक topic, PDF, या show episode देते हैं, और यह एक दो-host conversational podcast produce करता है। यह पूरी चीज़ Google के Gemini text-to-speech synthesis पर चलती है।
Audio pipeline इस तरह काम करता है:
- Source material से एक outline generate करें
- Outline को full script में expand करें
- Script को segments में divide करें
- Gemini TTS का use करके each segment के लिए audio synthesize करें
- Segments को stitch करें, loudness normalize करें, और final MP3 upload करें
Production default एक Gemini TTS model था जो ज़्यादातर use cases के लिए अच्छा काम करता था। लेकिन script जितना लंबा, उतना ज़्यादा आप segments के बीच voice drift सुन सकते थे। Segment एक में Host A, segment छह के Host A जैसा बिल्कुल नहीं लगता था। 25 मिनट के podcast के लिए, यह noticeable है लेकिन आप привык हो जाते हैं। 40 मिनट के करीब कुछ के लिए, यह quite rough हो जाता है।
मैंने एक अलग model configuration try करने का फैसला किया। Idea था कि लंबे podcasts में voice consistency improve की जाए।
मैंने क्या बनाया
अप्रैल के अंत में लगभग एक हफ़्ते में, मैंने एक अलग model configuration try करने के लिए commits की एक series push कीं। यह कोई casual one-line change नहीं था। मैंने एक पूरी architecture shift बनाई:
- Default TTS model को new configuration में switch किया
- एक fallback chain added -- अगर primary model timeout करे, तो stable production model पर fallback
- एक chunk-level QA system बनाया: transcripts को छोटी units में divide करें, each one को synthesize करें, ffmpeg analysis से audio quality validate करें
- Workflow progress tracking added ताकि UI per-segment synthesis state दिखा सके
- Retry logic को hardened किया -- प्रति chunk तीन attempts, exponential backoff
- एक long-form audio quality gate added जो
COMPLETEको block कर देता जब तक कि final assembled MP3 audio QA pass न करे
Idea था "हर segment के लिए audio generate करो और उम्मीद करो कि consistent लगे" से "transcripts को chunk करो, each chunk का QA करो, failures retry करो, final file validate करो" की तरफ बढ़ना।
28 अप्रैल तक, code production में deploy हो चुका था। Unit tests पास हुए। Integration tests पास हुए। जो मेरे पास नहीं था -- और होना चाहिए था -- वह था new configuration के खिलाफ एक full 50+ मिनट export चलाने वाला load test। मैंने वह एक test skip कर दिया जो actually matter करता था।
क्या टूटा
लगभग तुरंत, एक production podcast fail हो गया।
यह एक लंबा था -- Big Tech capex का एक analysis जो लगभग 65 मिनट की spoken audio की तरफ बढ़ रहा था। System audio step पर पहुँचा और AUDIO_FAILED return किया।
पहले, मैंने 409 Conflict देखा और सोचा कि orchestrator में एक bug है। पता चला कि 409 एक secondary symptom था -- podcast को already failed mark कर दिए जाने के बाद का retry attempt। पहली failure बहुत simpler थी।
Segment 0 ने new model configuration पर timeout कर दिया। Timeout 60 seconds पर set था। Fallback model -- जो वही model था जो stable path use करता है -- ने भी timeout कर दिया। System ने podcast को failed mark किया और आगे बढ़ गया।
Fallback को काम करना चाहिए था। लेकिन fallback को उसी 60-second timeout budget के साथ, उसी बड़े segment पर, उसी request context में invoke किया गया जो पहले ही failed primary call पर समय consume कर चुका था। Stable path, इसके विपरीत, उसी model को fresh call करता है full available timeout के साथ और segments को individually process करता है। Same model, different conditions -- इसलिए stable version इसे ठीक handle करता है लेकिन fallback नहीं।
मैंने production में chunk QA system को one-line change से disable कर दिया। इससे timeout fix नहीं हुआ। New configuration अभी भी उन segments पर timeout कर रही थी जिन्हें stable model बिना किसी issue के handle करता था।
खैर, यह उस तरह का moment है जहाँ आपको choose करना पड़ता है: new path को debug करते रहें, या वापस जाएँ उस पर जो काम करता है।
Rollback
30 अप्रैल को, मैंने call लिया।
Rollback commit (4a5bfc8) ने 2,724 lines code delete कीं:
- पूरा TTS chunker module
- Audio quality analysis gates
- Workflow progress tracking
- Chunk QA retry test suites
- Fallback model chain configuration
और 321 lines add कीं उन parts को preserve करने के लिए जो matter करते थे -- localized voice prompt phrasing, frontend compatibility, और stable path के लिए regression coverage।
Delete करने के लिए यह बहुत सारी lines हैं। honestly, यह defeat admit करने जैसा लगा। लेकिन ऐसा नहीं था। यह long-term सही path को choose करना था उस चीज़ के ऊपर जो progress जैसी लग रही थी लेकिन ready नहीं थी।
Rollback के बाद, मैंने:
- Shared base Docker image को rebuild किया
generate-speechservice को redeploy किया- Failed podcast को script approval पर reset किया
- Normal UI से "Generate Audio" click किया
Result:
- Model: Gemini TTS (stable production config)
- Segments: 6 में से 6 completed
- Duration: 1,527 seconds -- लगभग 25 मिनट
- Final MP3: 30.5 MB
- Status:
COMPLETE
वह podcast जो दो दिनों से अटका हुआ था, rollback के बाद लगभग 11 मिनट में finish हो गया। User का episode ship हो गया -- planned से दो दिन late, लेकिन complete।
65 मिनट वाले number के बारे में एक बात: वह raw script length पर based estimated duration था, not final output। Reset के बाद, script ने normal shortening pipeline से गुज़रा, और final assembled audio लगभग 25 मिनट का निकला। Original script और भी लंबा होता -- जो इसका भी हिस्सा था कि यह fail क्यों हो रहा था।
Data
यह decide करने से पहले कि आगे क्या करना है, मैं actual numbers चाहता था। Vibes नहीं -- data। मैंने podcast duration information के लिए production database को query किया।
| Cohort | Count | Median Duration | p90 | Max |
|---|---|---|---|---|
| पिछले 30 दिन | 8 | 26 min | 31 min | 34 min |
| पिछले 90 दिन | 72 | 27 min | 36 min | 45 min |
| All time | 120 | 26 min | 30 min | 45 min |
तो median podcast लगभग 26 मिनट का है। p90 लगभग 34-36 मिनट है। अब तक का सबसे लंबा completed podcast 45 मिनट का था।
और यहाँ harder data है -- podcasts जो audio stage तक पहुँचे, estimated duration के हिसाब से broken down:
| Estimated Duration | Reached Audio | Completed | Failed | In Progress |
|---|---|---|---|---|
| 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 Progress" उन podcasts को cover करता है जो शुरू हुए लेकिन कभी complete नहीं हुए -- user द्वारा abandoned, mid-generation cancelled, या pending state में छोड़े गए। 50+ मिनट पर दो podcasts attempt हुए। दोनों audio stage पर fail हुए। Zero completed।
किसी भी completed production podcast ने कभी 50 मिनट को reach नहीं किया है। System 15-40 मिनट की range के लिए अच्छा काम करता है। 40 मिनट से ऊपर, risk तेज़ी से बढ़ता है। और 50 मिनट से ऊपर, यह essentially untested territory है।
मैंने क्या सीखा
1. Configuration changes को production-level stress testing की ज़रूरत है
मुझे इसके बारे में honest होना होगा। New model configuration short test prompts में ठीक काम कर सकती थी। लेकिन production में, real scripts और real timeout constraints के साथ, यह उन segments पर fail हुई जिन्हें stable configuration बिना issue के handle करती है।
मैंने deploy करने से पहले ठीक से benchmark नहीं किया। एक short test prompt succeed करेगा। 60-second timeout के तहत multiple segments के साथ एक 65 मिनट का podcast बिल्कुल अलग context है। उन दो scenarios के बीच का gap ही वह जगह है जहाँ production incidents रहते हैं।
मैं configuration के बारे में ग़लत हो सकता हूँ -- शायद इसे बस ज़्यादा tuning या लंबे timeout की ज़रूरत थी। लेकिन point वही है: मैंने एक core production dependency को बिना उस stress testing के change कर दिया जिसका एक system जिस पर लोग actually rely करते हैं, हक़दार है।
2. Voice inconsistency असली problem है, timeouts नहीं
Timeout visible failure थी। लेकिन मैं जिस reason से first place में एक अलग configuration try कर रहा था, वह थी voice inconsistency। Stable model पर भी, synthesis calls के बीच voices shift होती हैं। Segment एक में Host A, segment छह में Host A जैसा बिल्कुल नहीं लगता।
Short podcasts के लिए, यह barely noticeable है। लंबे ones के लिए, यह accumulate होता है। और 50+ मिनट के podcasts के लिए -- जो, फिर से, किसी ने production में complete नहीं किया -- यह शायद really obvious होता।
Chunked approach इसे solve करने की कोशिश कर रहा था हर chunk को smaller और more controlled बनाकर। यह सही direction है, मुझे लगता है। Implementation बस अभी production के लिए ready नहीं थी।
3. मुझे incident से पहले telemetry चाहिए थी, बाद में नहीं
जब failure हुआ, मैं TTS cost या performance को specific podcast IDs से map नहीं कर पाया। Logs में useful entries नहीं थे। Workflow events में कोई TTS generation records नहीं थे।
मुझे podcast के status field, एक confusing 409 error, और timeout behavior के local reproduction से failure diagnose करना पड़ा। यह eventually काम किया, लेकिन यह उस तरह का debugging experience नहीं था जो मैं फिर से चाहता हूँ।
मैंने बाद में TTS cost telemetry add की -- model used, fallback model, retry count, per-attempt status, transcript characters, output audio bytes, audio duration। यह incident से पहले exist होना चाहिए था। मेरे experience से, यह हमेशा ऐसा ही है। आप observability fire के बाद बनाते हैं, पहले नहीं।
4. Rollback करना failure नहीं है
2,724 lines of code delete करना बुरा लगा। मैं pretend नहीं करूँगा कि नहीं लगा। आप एक चीज़ बनाने में एक हफ़्ता बिताते हैं, आपको architecture पर pride होता है, और फिर आप उसे सब तोड़ देते हैं क्योंकि वह ready नहीं है।
लेकिन यह सही call था। Chunk QA system अच्छा design था। यह वापस आएगा -- एक smaller, isolated change के रूप में proper canary validation के साथ। बस model configuration change के हिस्से के रूप में नहीं। और न ही तब जब new setup अभी भी उन segments पर timeout कर रहा हो जिन्हें stable path बिना issue के handle करता है।
5. 50 मिनट का episode एक अलग product है
यह वाला मुझे surprise किया। मैंने assume किया था कि system किसी भी length की script handle कर लेगा। Data कुछ और कहता है।
अगर कोई genuinely 50 मिनट का podcast चाहता है, तो वह एक अलग generation profile है। शायद एक script की ज़रूरत होगी जो audio generation से पहले 45 मिनट या उससे कम तक tightened हो जाए। TTS से पहले एक manual review gate। Stronger per-segment timeout budgets। शायद एक पूरी तरह अलग synthesis strategy भी।
15-40 मिनट की range के लिए default path को optimize करना सही call है। 50+ मिनट के episode को एक exceptional path के रूप में treat किया जाना चाहिए, norm के रूप में नहीं। मुझे लगता है कि यह एक product decision है, सिर्फ engineering नहीं।
चीज़ें अभी कहाँ हैं
Production stable TTS configuration पर वापस है। यह perfect नहीं है। लंबे podcasts पर voice inconsistency अभी भी है, और मैं उसे सुन सकता हूँ। लेकिन यह इतना stable है कि platform vast majority of use cases के लिए काम करता है।
अगर आप सुनना चाहते हैं कि DIALOGUE क्या produce करता है, तो आप खुद try कर सकते हैं https://podcast.chandlernguyen.com पर।
Incident report repository में committed है। Rollback exact SQL shape के साथ documented है failed podcast को reset करने के लिए। Telemetry अब next attempt के लिए in place है।
और lesson मुझे जितना अच्छा लगता उससे ज़्यादा clear है:
Production AI में, proper stress testing के बिना core dependencies change करने की cost एक failed experiment नहीं है। यह एक failed user experience है।
मैं different model configurations के साथ फिर try करूँगा। लेकिन अगली बार, better telemetry, एक canary deployment path, और एक stress test के साथ जो production को touch करने से पहले एक full 50+ मिनट export चलाए।
Production AI dependencies change करने से पहले, यह checklist है जिसे मुझे follow करना चाहिए था:
- Worst-case job का stress test करें। सबसे लंबा, सबसे भारी workload चलाएँ जिसकी आप expect करते हैं -- सिर्फ एक short test prompt नहीं। अगर आपका system 40 मिनट के podcasts handle करता है, तो 50 मिनट वाले से test करें।
- Day one से per-attempt telemetry log करें। Model name, fallback model, retry count, timeout, transcript characters, output audio bytes, audio duration। यह incident से पहले exist होना चाहिए था। हमेशा होता है।
- एक real job को canary करें। सबके लिए switch flip करने से पहले, एक single real production job को new config पर end to end चलाएँ और output verify करें।
- Same timeout budget के तहत fallback verify करें। एक fallback जो primary के same timeout को share करता है, fallback नहीं है -- वह same conditions में fail होने का दूसरा मौक़ा है।
- Deploy करने से पहले rollback SQL और runbook define करें। एक stuck job को reset कैसे करना है, exactly जानें। Commands को document करें। अगर आपके पास यह नहीं है, तो आप incident के दौरान improvising करेंगे।
अगर आप production AI systems build करते हैं, तो model configuration changes को test करने के बिना users को break किए आपका क्या pattern है? मैं genuinely जानना चाहूँगा। क्या आपका भी ऐसा "एक चीज़ बदली और सब टूट गया" moment रहा है?
Cheers,
Chandler





