Steven
Steven9 मिनट पढ़ें

ख़ामोशी भार-वहन कर रही थी

GeekBye v1 की आख़िरी दो releases एक ही असहज सच्चाई के बारे में हैं: एक असली network पर real-time transcription लॉसलेस नहीं है, और ईमानदार चाल है यह दिखावा बंद करना कि वह है। v1.8.20 ने एक reconnect के दौरान हर audio chunk को गिराने से पहले उसकी एक प्रति disk पर रखी, और transcript के अंतरालों को ज़ोर से चिह्नित करना शुरू किया। v1.9.0 ने bandwidth बचाने के लिए silence भेजना बंद किया — और पाया कि वह silence ही वह ठीक-ठीक संकेत था जिससे transcriber जानता था कि एक वाक्य ख़त्म हो चुका है। चीज़ें फेंक देने की क़ीमत के बारे में दो releases।

इंजीनियरिंग
Audio
विश्वसनीयता
GeekBye रिलीज़
ख़ामोशी भार-वहन कर रही थी

GeekBye v1 की आख़िरी दो releases — v1.8.20 और v1.9.0, version-2 पड़ाव से ठीक पहले वाली — एक ऐसी थीम साझा करती हैं जो तभी स्पष्ट होती है जब आप उन्हें साथ में पढ़ें। दोनों उन चीज़ों के बारे में हैं जिन्हें app चुपचाप फेंक रहा था, और दोनों एक ही निष्कर्ष पर पहुँचती हैं: जिसे आप efficiency के लिए गिराते हैं वह आपके सोचे से ज़्यादा काम कर रहा था। एक release network reconnects के दौरान audio गिरा रही थी। दूसरी bandwidth बचाने के लिए silence गिरा रही थी। दोनों ही सूरतों में, गिराई गई चीज़ असल में भार-वहन करने वाली निकली।

Real-time transcription लॉसलेस नहीं है, और v1.8.20 इसे मानती है

एक WebSocket पर streaming speech-to-text के बारे में एक ऐसा तथ्य है जिससे नज़र फेर लेना आसान है: जब connection गिरता है और फिर जुड़ता है — या जब app एक transcription provider से दूसरे पर, Deepgram से ElevenLabs पर failover करता है — तो client उस अंतराल के दौरान पकड़े गए audio chunks को जानबूझकर गिरा देता है। उसे गिराना ही पड़ता है। एक ताज़ा-जुड़ा provider जो अचानक बासी audio का एक ढेर पा जाए वह घुटेगा, या इससे बुरा, अपने voice-activity detector को ग़लत चीज़ पर जकड़ लेगा। तो client उन chunks को फेंक देता है।

समस्या गिराना नहीं है। समस्या यह है कि गिराने का पहले क्या अर्थ था: वे शब्द बस transcript में कभी आते ही नहीं थे, और कुछ भी आपको नहीं बताता था कि वे ग़ायब हैं। recording पूर्ण दिखती थी। वह नहीं थी।

v1.8.20 इस पर दो मोर्चों से हमला करती है, और उनमें से कोई भी «reconnects को कभी audio न खोने देना» नहीं है — क्योंकि आप ऐसा नहीं कर सकते, real-time में नहीं। इसके बजाय वह इस नुक़सान को वसूली-योग्य और ईमानदार बना देती है।

वसूली-योग्य आधा एक टिकाऊ PCM tee है। main-process capture handler में, हर पकड़ा गया chunk अब disk पर एक file में लिखा जाता है इससे पहले कि वह reconnect और grace-period वाले drop gates तक पहुँचे:

every captured chunk is written to <userData>/sessions/session-<ts>.wav before realtime drop gates so reconnect-lost audio is preserved for a future batch-STT reconciliation pass

यंत्र-रचना सुखद रूप से लो-टेक है: teeChunkToDisk एक WriteStream खोलता है, आगे-आगे एक 44-बाइट WAV header आरक्षित करता है, कच्चे 16-बिट mono samples को जैसे-जैसे वे आते हैं जोड़ता जाता है, और रुकने पर header को अंतिम आकारों के साथ patch करता है (fs.openSync(..., 'r+'), offset 0 पर 44 बाइट लिखो)। नतीजा पूरे audio की एक चलने-योग्य WAV है — उन हिस्सों समेत जिन्हें real-time transcript ने कभी नहीं देखा। जब recording ख़त्म होती है तो वह file path और byte गिनती के साथ एक session-audio-saved event निकालता है।

यहाँ दो ईमानदारी की टिप्पणियाँ मायने रखती हैं, क्योंकि इसे बढ़ा-चढ़ाकर कहना आसान है। पहला: tee इन releases में अंतराल भरता नहीं है। वह कच्चे audio को एक बाद के मिलान-पास के लिए बचाता है — एक batch री-ट्रांसक्रिप्शन जो file को live transcript से मिलाता है — जो एक हुक है जिसे ये releases बिछाती हैं, न कि एक feature जिसे वे भेजती हैं। follow-up commit तो session-audio-saved पर मिलान वाले issues की ओर इशारा करती एक TODO टिप्पणी तक जोड़ती है ताकि कोई इसे मुर्दा code न समझ बैठे। दूसरा: यह disk ख़र्च करता है। टीम ने मोटे तौर पर प्रति 30-मिनट recording क़रीब 86 MB आँका, तो वही release एक StartupCleanupService जोड़ती है जो 7 दिन से पुरानी सहेजी गई sessions को छाँट देता है। एक ऐसी सुरक्षा-जाली जो बिना सीमा बढ़ती है एक रिसाव है; वह सफ़ाई ही वह चीज़ है जो tee को भेजने-योग्य बनाती है।

ईमानदार आधा तो और भी सरल है। पहले reconnect प्रयास पर, client transcript में एक असली, दिखने वाली entry लिखता है:

[reconnecting — audio may be missing]

वह एकमात्र पंक्ति इस release के पूरे दर्शन का एक लघुचित्र है। app इस बात की गारंटी नहीं दे सकता कि उसने सब कुछ सुना, तो वह यह जताना बंद कर देता है कि उसने सुना। एक अंतराल जो आप देख सकते हैं वह एक अंतराल है जिसके इर्द-गिर्द आप काम कर सकते हैं; एक अंतराल जो आप नहीं देख सकते वह एक bug रिपोर्ट है जो घटित होने की प्रतीक्षा में है।

v1.9.0: वह silence जिसे आपने optimize करके हटा दिया वही संकेत था

v1.9.0 आख़िरी v1 release है, और यह वह जगह है जहाँ backend ने live-transcript की ज़िम्मेदारियों का एक सेट client को सौंप दिया — silence गिराना, हर transcript पंक्ति को एक स्थानीय voice-activity timeline से mic या system audio को श्रेय देना, reconnect loop का स्वामी होना, recording सीमाएँ लागू करना। उसका अधिकांश version-2 architecture के लिए ज़मीन-तैयारी है और कहीं और बताया जा चुका है। पर उसमें एक सूत्र दोनों releases में सबसे अच्छी छोटी कहानी है, क्योंकि यह एक ऐसा fix है जिसे एक ऐसे optimization की क्षति को पलटना पड़ा जो अलग-थलग देखने पर पूरी तरह सही थी।

यह एक स्पष्ट रूप से अच्छे बदलाव से शुरू होती है: silence भेजना बंद करो। client ने उन chunks को, जिनका प्रमुख स्रोत silence था, socket पर भेजने से पहले छानना शुरू किया — कम bandwidth, backend को process करने के लिए कम बेकार audio, और उसने एक स्पष्ट backend client-contract का पालन किया जो कहता था «silence मत भेजो»। इसे करने का हर कारण ठोस था।

और उसने utterance finalization को पूरी तरह तोड़ दिया। जो commit इसे ठीक करती है वह उस कारण-श्रृंखला को असाधारण स्पष्टवादिता से बिछा देती है:

After the client began filtering silence chunks … ElevenLabs Scribe's VAD-based commit strategy no longer fires — EL needs ~1s of silence in the audio stream to commit a partial, and that silence never reaches it. Result: partials accumulate forever, UI shows trailing "...", utterances never finalize.

यह एक रिसते abstraction के पलटकर काटने का आदर्श रूप है। transcriber का voice-activity detector कोई अलग «वक्ता ख़त्म कर चुका» input नहीं रखता — वह audio में क़रीब एक सेकंड की silence सुनने से एक वाक्य के अंत का अनुमान लगाता है। silence तार पर शोर नहीं था; वह एक संदेश था। bandwidth बचाने के लिए उसे छान देकर, client ने ग़लती से वह इकलौता संकेत भेजना बंद कर दिया जो transcriber को बताता था कि एक वाक्य ख़त्म हो चुका है। तो हर partial transcript बस बढ़ता ही गया, पूँछ एक ऐसे ... में घिसटती हुई जो कभी हल नहीं हुई।

आप इसे silence को वापस डालकर ठीक कर सकते थे — पर तब आपने optimization को उलट दिया। असल fix बेहतर है: silence भेजे बिना silence का अर्थ भेजो। client अब स्थानीय रूप से ट्रैक करता है कि audio कितनी देर से ख़ामोश है, और जब वह SILENCE_COMMIT_THRESHOLD_MS = 1000 को पार करता है — जानबूझकर backend के अपने VAD silence threshold से मिलाया गया — तो वह एक स्पष्ट commit संकेत out of band दागता है: sendMixedChunk लगातार silence को ताड़ता है, IPC पर एक realtime-audio:commit भेजता है, main process socket पर एक { type: 'commit' } आगे बढ़ाता है, और backend ElevenLabs से partial को अंतिम रूप देने को कहता है। दो छोटे state के टुकड़े, silenceStartedAt और silenceCommitFired, इसे बार-बार दगने से रोकते हैं। bandwidth की जीत बची रहती है; finalization फिर से काम करता है; अंत-वाला ... हल हो जाता है।

इस release का एक छोटा, ईमानदार पादटिप्पणी भी है: वह अनबाउंडेड reconnect logic जिसे v1.9.0 ने पेश किया पहले renderer में एक WsReconnectController के रूप में बनाया गया था, फिर उसी release के भीतर हटा दिया गया जब असली implementation main-process handler में उतरा जहाँ उसका ठौर था। reconnect की कहानी अपने आप में — अनबाउंडेड backoff, हर पाँच नाकामियों पर provider अदल-बदल — अपने अलग अध्याय में बताई गई है; v1.9.0 जो जोड़ती है वह यह है कि यहीं वह architecture जन्मा, और यहीं client ने पहली बार उसका स्वामित्व लिया।

तीन चीज़ें जो इन releases ने सिखाईं

  1. जब तुम नुक़सान रोक नहीं सकते, तो उसे वसूली-योग्य बनाओ और उसे दृश्यमान बनाओ। v1.8.20 reconnects को audio गिराने से नहीं रोकती — वह real-time में भौतिक रूप से संभव ही नहीं है। वह कच्चे audio की एक प्रति बाद के लिए रखती है, और वह transcript में अंतराल पर लेबल लगाती है। कोई भी चमक-दमक वाला नहीं; साथ मिलकर वे ख़ामोश data नुक़सान को एक ज्ञात, परिबद्ध, वसूली-योग्य स्थिति में बदल देते हैं।
  2. एक सुरक्षा-जाली को अपनी सीमाएँ चाहिए। टिकाऊ tee अगर अकेला छोड़ दिया जाए तो चुपचाप disk भर देगा — एक ऐसा fix जो एक स्लो-मोशन bug बन जाता है। उसी release में भेजी गई 7-दिन की सफ़ाई कोई बाद-की-सोच नहीं है; वही वह चीज़ है जो tee को क़तई भेजने लायक़ सुरक्षित बनाती है। अगर तुम एक ऐसा तंत्र जोड़ते हो जो जमा करता है, तो उसे छाँटने वाला तंत्र उसी साँस में जोड़ो।
  3. जिसे तुम efficiency के लिए गिराते हो वह एक संकेत हो सकता है। दोनों releases का सबसे पैना इकलौता सबक़: silence को छान देना हर उस धुरी पर सही था जिसे तुम आमतौर पर जाँचते — bandwidth, load, contract अनुपालन — और फिर भी उसने product तोड़ दिया, क्योंकि एक downstream system उस silence में अर्थ पढ़ रहा था। जब तुम efficiency के लिए data हटाते हो, तो पूछो कि उस data से कौन कुछ अनुमान लगा रहा था। कभी-कभी तुम्हें अर्थ स्पष्ट रूप से भेजना पड़ता है, ठीक इसलिए कि तुमने वह चीज़ भेजनी बंद कर दी जिससे वह पहले अनुमानित होता था।

और यही version-1 की कहानी का अंत है — फिर से लिखने से पहले की आख़िरी मज़बूती। पिछले अध्याय के लिए, देखें तीन क्रियाएँ जो web audio को ज़िंदा रखती हैं (v1.8.16 + v2.0.2); यह सब जहाँ जा रहा था उसके लिए, एक असली version 2 में सचमुच क्या लगता है: 206 commits (v2.0.0); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना

संबंधित लेख

तीन क्रियाएँ जो Web Audio को ज़िंदा रखती हैं
Steven
Steven10 मिनट पढ़ें

तीन क्रियाएँ जो Web Audio को ज़िंदा रखती हैं

GeekBye की दो point releases, दो महीने के फ़ासले पर और दो अलग files में, हमारे audio code को उलटे छोरों से एक ही सबक़ सिखा गईं: browser के AudioContext को इस्तेमाल-कर-फेंको जैसा मानना बंद करो। एक release ने एक ऐसे context को resume() करना सीखा जिसे macOS ने record के बीचोंबीच चुपचाप suspend कर दिया था; दूसरी ने close() के बजाय suspend() करना सीखा ताकि पीठ-दर-पीठ चलती sessions Chromium की लगभग-छह-context वाली छत से टकराना बंद कर दें। resume, suspend, close — यही पूरी कहानी है।

इंजीनियरिंग
Audio
डेस्कटॉप
एक कनेक्शन टूटने से पूरा ऐप क्रैश नहीं होना चाहिए — पर हमारा हुआ
Steven
Steven7 मिनट पढ़ें

एक कनेक्शन टूटने से पूरा ऐप क्रैश नहीं होना चाहिए — पर हमारा हुआ

जब हमारा बैकएंड मीटिंग के बीच में ऑफ़लाइन हुआ, तो उसने सिर्फ़ ट्रांसक्रिप्शन नहीं रोका — उसने पूरा ऐप क्रैश कर दिया। वजह थी एक अकेली अनहैंडल्ड इवेंट, और फ़िक्स दस लाइनों का था। यह उन रिलीज़ों का गुच्छा है जिसने GeekBye को उन हालात से गुज़रते हुए भी साइन-इन बनाए रखा और कनेक्टेड बनाए रखा, जो पहले उसे मार डालते थे।

इंजीनियरिंग
विश्वसनीयता
Electron
वो सेफ्टी चेक जिसने हमारी ऐप को क्विट करना नामुमकिन बना दिया
Steven
Steven7 मिनट पढ़ें

वो सेफ्टी चेक जिसने हमारी ऐप को क्विट करना नामुमकिन बना दिया

ऑटो-अपडेट अब तक का सबसे मुश्किल फीचर था जो हमने शिप किया — चार दिन में छह रिलीज़ सिर्फ इसे ऐप को ब्रिक करने से रोकने के लिए। सबसे बुरा बग वो था जो हमने खुद सावधान रहने की कोशिश में पैदा किया: एक 500-मिलीसेकंड का "सेफ्टी" चेक जिसने एक फेल्ड अपडेट को ऐसे प्रोसेस में बदल दिया जिसे आप शाब्दिक रूप से क्विट ही नहीं कर सकते थे।

इंजीनियरिंग
Electron
विश्वसनीयता