Steven
Steven9 मिनट पढ़ें

OCR से pixels तक, fallback गँवाए बिना

GeekBye ने आपके screenshots को OCR से पढ़ना बंद किया और असली image को vision models तक भेजना शुरू किया — और वह आसान हिस्सा है। सीखने लायक़ हिस्सा वह optimization है जिसने vision users के लिए OCR को पूरा छोड़ दिया, non-vision models तक के fallback को चुपके से तोड़ दिया, और उसे एक ऐसे version तक वापस समेटना पड़ा जहाँ safety net समानांतर में चलता है और कुछ भी ख़र्च नहीं करता।

इंजीनियरिंग
AI
डेस्कटॉप
GeekBye रिलीज़
OCR से pixels तक, fallback गँवाए बिना

कोई भी ऐसी app जो आपको किसी चीज़ का screenshot लेकर उसके बारे में AI से पूछने देती है, उसके सामने एक सच्चा दोराहा है। आप OCR चला सकते हैं — text को device पर ही निकालो, एक बहुत छोटा string भेजो, और तस्वीर फेंक दो। या आप pixels भेज सकते हैं — असली image रवाना करो और एक vision-capable model को उसे पढ़ने दो। OCR सस्ता और छोटा है पर हर उस चीज़ के लिए अंधा है जो text नहीं: layout, diagram, code का indentation, error के नीचे की लाल लहरदार लकीर। pixels उस सबको बचा लेते हैं पर ज़्यादा bytes लेते हैं और एक ऐसे model की माँग करते हैं जो देख सके। GeekBye उस दोराहे के OCR वाले किनारे से शुरू हुई और, v1.8.6 तथा v1.8.7 के आर-पार, pixels वाले किनारे पर चलकर आ गई। वह बदलाव इस कहानी का आसान हिस्सा है। दिलचस्प हिस्सा वह optimization है जो उसके साथ आई, fallback को तोड़ा, और जिसे पलटना पड़ा।

पहले: text पढ़ो, तस्वीर छोड़ दो

इस सिलसिले से पहले, एक screenshot ऐसे जवाब बनता था: आप capture shortcut दबाते हैं, client OCR चलाता है — macOS पर एक Swift VisionOCR binary के ज़रिए Apple का Vision framework, Windows पर एक PowerShell script, दोनों एक ही IOcrEngine abstraction के पीछे — और निकाला गया text एक string में लपेटकर screenshotText के रूप में backend को भेज दिया जाता है। image ख़ुद कभी machine से बाहर नहीं गई। वह efficient है, और सादे text की एक दीवार के लिए ठीक है। पर एक screenshot शायद ही कभी सिर्फ़ text होता है। वह दो-column layout वाला एक job posting है, एक diagram वाली coding problem है, एक ऐसी error है जिसके stack trace का indentation ही सुराग़ है। OCR उस सबको एक-एक line की नक़ल में चपटा कर देता है और model आपके पूछे से पतले सवाल का जवाब देता है।

बदलाव: pixels भेजो (v1.8.6)

v1.8.6 ने दूसरा रास्ता जोड़ा। एक नया imageOptimizer.ts, जो sharp पर बना है और इसलिए cross-platform है, एक कच्चे screenshot के साथ ठीक तीन चीज़ें करता है:

sharp(pngFilePath).resize({ width: 1920, withoutEnlargement: true }).jpeg({ quality: 80 })

ज़्यादा से ज़्यादा 1920px चौड़ा resize करो (किसी छोटे capture को कभी upscale मत करो), PNG को JPEG में बदलो, quality 80. output कच्चा base64 है जिसका media type एक अलग field में रखा जाता है, और वह पुराने वाले के साथ-साथ दो नए request fields में सवार होकर backend तक जाता है: screenshotText से स्वतंत्र, screenshotImage और screenshotImageMediaType. backend अब एक image, text, या दोनों पा सकता है।

वह optimization वाला step दिखावटी नहीं है। एक कच्चा screenshot PNG 3–8 MB का होता है; resize किया गया JPEG मोटे तौर पर 200–500 KB. वह upload पर एक order of magnitude की कटौती है, और यह model का बिल भी घटाता है — vision models एक image को उसके resolution के हिसाब से tile में बाँटते हैं और per tile charge करते हैं, तो चौड़ाई को 1920 पर सीमित करना और दोबारा compress करना, wire पर के bytes और जिन image tokens के आप पैसे देते हैं, दोनों को सीधे घटा देता है — screen पर के text की पठनीयता में कोई मतलब भर का नुक़सान किए बिना।

तो app को यह पता ही कैसे चलता है कि model देख भी सकता है या नहीं? backend उसका ऐलान करता है। /api/config के response में plan के हिसाब से key किया गया एक ai.vision block जुड़ गया, और client हर config fetch पर एक अकेला boolean — aiSupportsVision — cache करता है। जब vision support होता है, image optimize करो; जब नहीं होता, OCR पर fallback करो। तो capability एक backend तथ्य है, जो client-side cache होता है, और OCR चलाने या छोड़ने का हर screenshot वाला फ़ैसला उस flag से client पर लिया जाता है। असल में कौन सा model जवाब देता है, यह अब भी backend के हाथ में है।

वह optimization जो हद से ज़्यादा चतुर थी

यहीं यह सीख देने वाला बनता है। vision path के पहले रूप ने OCR और image optimization को एक साथ, समानांतर चलाया। फिर एक "perf" commit ने उसे या-यह-या-वह में कस दिया: अगर plan vision support करता है, तो image optimize करो और OCR को पूरा छोड़ दो; अगर नहीं करता, तो OCR चलाओ और image वाला काम छोड़ दो। काग़ज़ पर यह साफ़ जीत है — जिस text की vision model को ज़रूरत ही नहीं, उसे निकालें क्यों? changelog तो इसका इश्तिहार तक देता है: "OCR is skipped when the AI can read the image directly."

दिक़्क़त fallback शब्द में है। backend के पास एक model नहीं है; उसके पास एक primary और एक fallback है, और वह fallback एक non-vision model हो सकता है। इस क्रम की तस्वीर देखिए: आपके plan का primary model vision support करता है, तो client गर्व से OCR छोड़ देता है और सिर्फ़ image भेजता है। primary provider को हिचकी आती है। backend एक text-only model पर fallback करता है — जिसके हाथ में अब एक ऐसी image थमा दी गई है जिसे वह पढ़ नहीं सकता और कोई text नहीं, क्योंकि client ने text को optimize करके हटा दिया। fast path ने चुपके से safety net मिटा दी थी। एक फ़ालतू fast path आप सुरक्षित रूप से हटा सकते हैं; पर वह चीज़ आप नहीं हटा सकते जिसके इर्द-गिर्द से fast path एक shortcut था।

fix: primary पर नहीं, fallback पर टिकाओ

यह fix ही चुरा लेने लायक़ हिस्सा है। यह हमेशा OCR चलाने पर लौटना नहीं था — वह आम मामले की जीत को फेंक देता। यह छोड़ने को एक दूसरे capability flag पर सशर्त बनाना था: क्या fallback vision support करता है? backend के config में एक fallbackVision block जुड़ा; client पहले flag के बग़ल में aiFallbackSupportsVision cache करता है। अब फ़ैसले की चार शाखाएँ हैं:

  • primary और fallback दोनों देखते हैं → image optimize करो, OCR छोड़ो। तेज़, आम रास्ता।
  • primary देखता है पर fallback अंधा है → image optimize करो और OCR चलाओ, fallback के लिए बीमे के तौर पर।
  • image optimization exception फेंकती है → एक पक्के fallback के रूप में OCR।
  • plan में vision है ही नहीं → सिर्फ़ OCR।

और दूसरी शाखा — बीमे वाला मामला — वही है जहाँ आख़िरी चाल मायने रखती है। image optimization के बाद OCR चलाना उसकी latency को ऊपर से फिर जोड़ देगा। तो दोनों एक ही Promise.all के तहत एक साथ चलते हैं: image optimization और OCR ओवरलैप करते हैं, और वह OCR जो सिर्फ़ इस सूरत में है कि कहीं backend को ज़रूरत पड़ जाए, वैसे भी उसी वक़्त के अंदर ख़त्म हो जाता है जिसमें image optimize हो ही रही थी। safety net, जब तक इस्तेमाल न हो, wall-clock के लिहाज़ से मुफ़्त है। पूरी तरक़ीब एक वाक्य में: सस्ते fallback को रखो, उसे इस बात पर टिकाओ कि आपका fallback provider उसे सचमुच चाहता है या नहीं, और उसे समानांतर कर दो ताकि जिस दिन वह आपको बचाए उस दिन तक वह कुछ भी ख़र्च न करे।

दो छोटे जाल जो skip ने छेड़ दिए

टाले गए काम को आक्रामक तरीक़े से छोड़ देने की एक आदत है कि वह हर उस जगह को सतह पर ले आता है जिसने चुपचाप मान रखा था कि वह काम होगा ही। उसी हफ़्ते PR review में दो निकले।

एक queue deadlock. screenshot queue एक item को तब "तैयार" मानता था जब उसके पास OCR text होता। vision screenshots को अब — design के हिसाब से — कभी OCR text मिलता ही नहीं, तो पूर्णता की जाँच hasIncompleteOCR हमेशा के लिए इंतज़ार करती रही और queue अटक गया। fix "तैयार" को दोबारा परिभाषित करता है एक optimize की हुई image है या OCR text है के रूप में, जो अब pipeline असल में गारंटी करता है।

एक बासी capability flag. aiSupportsVision cache होता है, जिसका मतलब है कि वह ठीक ग़लत दिशा में बासी हो सकता है: एक user logout करता है, या एक config fetch fail होता है, और client यह मानता हुआ छूट जाता है कि वह अब भी एक ऐसे model को image भेज सकता है जो उन्हें अब support नहीं करता। flag अब logout पर और config-fetch failure पर reset होता है — एक cache की हुई capability के पास इसके लिए एक कहानी होनी चाहिए कि जब वह चीज़ ही चली जाए जिसका वह बयान करती है तब क्या।

संख्याओं पर एक बात

क्योंकि ईमानदारी इस series का मक़सद है: commit messages आँकड़े ढोते हैं — image optimization "~30ms" बनाम OCR "~100–150ms," payloads "~200–500 KB vs ~3–8 MB PNG" — और वे मानने लायक़ हैं, पर वे लेखक के अपने inline अनुमान हैं, किसी benchmark का output नहीं। history में कहीं भी कोई मापी गई end-to-end पहले/बाद वाली latency नहीं है। speed वाली दलील को एक पुख़्ता design मंशा मानिए, कोई साबित नतीजा नहीं। payload-size की कमी वह एक संख्या है जिस पर आप भरोसा कर सकते हैं, क्योंकि वह resize और दोबारा compress करने से सीधे निकल आती है।

इस release ने हमें तीन बातें सिखाईं

  1. pixels भेजना text पढ़ने को हरा देता है — जब model देख सके। OCR वह सब कुछ फेंक देता है जो एक screenshot characters के अलावा कहता है। एक vision model layout, diagram, और indentation पढ़ता है, और एक optimize किया हुआ JPEG उसे किफ़ायती बना देता है। पर यह capability पर एक दाँव है, इसीलिए इसे एक fallback चाहिए।
  2. आप एक fast path को optimize करके हटा सकते हैं, safety net को कभी नहीं। vision users के लिए OCR छोड़ना primary के लिए सही और fallback के लिए ग़लत था। bug skip नहीं था; bug यह था कि skip primary की capability के आधार पर किया गया जबकि मायने fallback की रखती थी।
  3. एक मुफ़्त safety net वही है जो समानांतर की गई हो। बीमे वाले काम को इस पर टिकाओ कि वह सचमुच ज़रूरी है या नहीं, फिर उसे fast path के साथ-साथ चलाओ ताकि वह ओवरलैप करे। ठीक से किया जाए, तो graceful degradation उस दिन तक कुछ भी ख़र्च नहीं करता जिस दिन वह आपके feature के अब भी चलने की इकलौती वजह होता है।

v1 कहानी के पिछले अध्याय के लिए, देखें एक click-through overlay के दो failure modes (v1.8.5); और पूरे चाप के लिए, सॉफ़्टवेयर को परिपूर्णता तक पहुँचाकर भेजने की शारीर-रचना

संबंधित लेख

एक click-through overlay के दो failure modes
Steven
Steven10 मिनट पढ़ें

एक click-through overlay के दो failure modes

GeekBye की window हर चीज़ के ऊपर तैरती है और आपके clicks को अपने आर-पार जाने देती है — सिवाय वहाँ जहाँ उसके buttons हैं। यह एक दो-तरफ़ा contract है, और v1.8.5 तथा v1.8.14 वही हैं जो हर तरफ़ के टूटने पर दिखता है: एक release जहाँ overlay ने एक system dialog निगल लिया, एक जहाँ उसने आपके keystrokes चुरा लिए। दूसरे का जीतने वाला fix code मिटाना था।

इंजीनियरिंग
Electron
डेस्कटॉप
release को CI में डालना, दो बार
Steven
Steven9 मिनट पढ़ें

release को CI में डालना, दो बार

GeekBye v1.8.4 ने macOS और Windows दोनों के release builds को CI में ले जाया। changelog जो नहीं बताता वह यह है: हमने यह एक बार पहले भी आज़माया था, runner cost की वजह से एक महीने बाद उसे मिटा दिया, और दूसरी बार में ही जाकर वह टिका — क्योंकि तब तक release script हाथ से पहले ही चल रहा था।

इंजीनियरिंग
CI/CD
डेस्कटॉप
बिना किसी safety net के तीस भाषाएँ ship करना
Steven
Steven9 मिनट पढ़ें

बिना किसी safety net के तीस भाषाएँ ship करना

GeekBye v1.8.3 ने react-i18next को जोड़ा और पूरे app को 30 भाषाओं में translate कर दिया। library वाला हिस्सा रवायती था। दिलचस्प हिस्सा वह है जो ग़ायब था: न कोई key-parity check, न कोई string extractor — इसलिए hardcoded English रिस गई, एक Danish typo ship हो गया, और changelog यह तय ही नहीं कर पाया कि app 28, 29 या 30 भाषाएँ बोलता है।

इंजीनियरिंग
i18n
डेस्कटॉप