Android पर प्राइवेसी-फर्स्ट OCR: Subly बिना क्लाउड के बिल कैसे पढ़ता है
2026 के लिए Android पर ऑन-डिवाइस OCR की व्यावहारिक गाइड, ML Kit Text Recognition के साथ — सेटअप, कोड, और वे फ़ील्ड-एक्सट्रैक्शन तरकीबें जो यूज़र के डेटा को आपके सर्वर से दूर रखती हैं।
जिस पल कोई यूज़र अपना फ़ोन किसी बिल की ओर करता है, आपके ऐप के पास एक विकल्प होता है। आप उस इमेज को किसी क्लाउड OCR सर्विस पर भेज सकते हैं और 400 ms में साफ़-सुथरा टेक्स्ट वापस पा सकते हैं — इसकी कीमत यह है कि किसी की निजी वित्तीय दस्तावेज़ को आपके सर्वर और एक थर्ड पार्टी से होकर गुज़ारना पड़ता है। या फिर आप यह काम डिवाइस पर ही कर सकते हैं, एक ज़्यादा ईमानदार प्राइवेसी पॉलिसी लिख सकते हैं, और आपको कभी यह सोचना नहीं पड़ेगा कि किसी अगले डेटा ब्रीच के लिए आप ज़िम्मेदार हैं।
मैंने दूसरा रास्ता चुना Subly के लिए, जो एक Android सब्सक्रिप्शन ट्रैकर है और आपको Netflix या Spotify का इनवॉइस स्कैन करने देता है और सर्विस का नाम, राशि, फ़्रीक्वेंसी, और अगली चार्ज तारीख अपने आप भर देता है। कुछ भी फ़ोन से बाहर नहीं जाता। यह पोस्ट बताती है कि 2026 में यह कैसे काम करता है — सेटअप, कोड, और वे फ़ील्ड-एक्सट्रैक्शन तरकीबें जो मैंने मुश्किल तरीके से सीखीं।
ऑन-डिवाइस क्यों, जब क्लाउड इतना आसान है
क्लाउड OCR APIs एक ही लाइन की बात हैं। Google Cloud Vision, AWS Textract, Azure Read — ये सभी आपको साफ़ नतीजे देते हैं और ये सभी आपके यूज़र्स की इमेज चाहते हैं। एक सब्सक्रिप्शन ट्रैकर के लिए, उन इमेज में शामिल हो सकता है:
- अकाउंट होल्डर का नाम, आख़िरी 4 अंक, या पूरा ईमेल
- प्रिंटेड बिल के नीचे के पते वाले हिस्से
- पेमेंट मेथड की जानकारी
- पूरी कीमत, लाइन आइटम, और तारीख़ें
इस सूची में कुछ भी ऐसा नहीं है जिसे मैं किसी थर्ड-पार्टी के लॉग में पड़ा हुआ देखना चाहूँ। और प्रोडक्ट के नज़रिए से, “हम आपके बिल कभी कहीं नहीं भेजते” एक “हम आपके बिल Google Cloud पर भेजते हैं और उन्हें 30 दिन के अंदर डिलीट कर देते हैं” से कहीं ज़्यादा मज़बूत मार्केटिंग लाइन है। तो आर्किटेक्चर उसी वैल्यू का अनुसरण करता है: इमेज को डिवाइस पर रखें, केवल एक्सट्रैक्ट किए गए फ़ील्ड्स को सेव करें, और स्कैन के लिए इंटरनेट कनेक्शन बिल्कुल न खोलें।
सबसे प्राइवेट बाइट वही है जो कभी फ़ोन से बाहर नहीं जाती।
ML Kit Text Recognition v2: असली काम करने वाला
Google का ML Kit Text Recognition v2, 2026 में, Android पर ऑन-डिवाइस OCR के लिए उबाऊ-लेकिन-सही जवाब है। यह Latin मॉडल के साथ आता है जो आपके ऐप में बंडल होता है (लगभग 4 MB), एक Pixel-क्लास फ़ोन पर दसियों मिलीसेकंड में चलता है, और प्रिंटेड टेक्स्ट को रसीदों, इनवॉइस, और ईमेल स्क्रीनशॉट के लिए काफ़ी अच्छे से हैंडल कर लेता है।
इसमें मुझे जो पसंद है:
- किसी नेटवर्क परमिशन की ज़रूरत नहीं। आप
INTERNETहटाकर एक ऐप शिप कर सकते हैं और OCR फिर भी काम करता है। सिर्फ़ यही इंटीग्रेशन में लगने वाले समय के लायक है। - ऑफ़लाइन चलता है। एयरप्लेन-मोड स्कैन एक फ़ीचर है, बग नहीं।
- प्रति-ऐप मॉडल। मॉडल को बंडल करने का मतलब है पहली बार लॉन्च पर कोई अचानक डाउनलोड नहीं और यूज़र का Wi-Fi लड़खड़ाने पर कोई
Try Againडायलॉग नहीं। - मुफ़्त। जैसे-जैसे आप स्केल करते हैं कोई API लागत नहीं। एक इंडी ऐप के लिए, यही लॉन्च करने और ज़रूरत से ज़्यादा सोचने के बीच का फ़र्क़ है।
इसका ट्रेड-ऑफ़ एज केस पर सटीकता है: हस्तलेखन पर भरोसा नहीं, घने थर्मल-प्रिंटेड रसीदें लेआउट को उलझा सकती हैं, और नॉन-Latin स्क्रिप्ट्स को एक अलग मॉडल चाहिए। रसीद/इनवॉइस/ईमेल वाले उपयोग के लिए, यह ज़रूरत से कहीं ज़्यादा अच्छा है।
2026 में सेटअप
Gradle डिपेंडेंसी:
// app/build.gradle.kts
dependencies {
implementation("com.google.mlkit:text-recognition:16.0.1")
implementation("androidx.camera:camera-camera2:1.4.0")
implementation("androidx.camera:camera-lifecycle:1.4.0")
implementation("androidx.camera:camera-view:1.4.0")
}
और आपके मैनिफ़ेस्ट में एक लाइन ताकि Play पहचान सके कि मॉडल बंडल है और समय से पहले डाउनलोड हो चुका है:
<application>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
</application>
बस इतना ही पूरा सेटअप है। कोई API की नहीं, कोई सर्विस अकाउंट JSON नहीं, कोई साइन किए गए रिक्वेस्ट नहीं।
CameraX फ़्रेम से टेक्स्ट पहचानना
यहाँ मुख्य लूप है। CameraX आपको ImageProxy फ़्रेम देता है, आप एक को InputImage में लपेटते हैं, और रिकग्नाइज़र आपको एक Text ऑब्जेक्ट लौटाता है जिसमें पैराग्राफ़, लाइनें, और रेक्टैंगल होते हैं।
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.latin.TextRecognizerOptions
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
private val recognizer = TextRecognition.getClient(
TextRecognizerOptions.DEFAULT_OPTIONS,
)
@androidx.camera.core.ExperimentalGetImage
fun analyzeFrame(image: ImageProxy, onScanned: (String) -> Unit) {
val media = image.image ?: return image.close()
val input = InputImage.fromMediaImage(media, image.imageInfo.rotationDegrees)
recognizer.process(input)
.addOnSuccessListener { result ->
// Whole page as a single string, line breaks preserved.
onScanned(result.text)
}
.addOnCompleteListener { image.close() }
}
कुछ बातें जानने लायक हैं:
- हमेशा
image.close()को ठीक एक बार कॉल करें, एरर पर भी। ML Kit का लिसनर पैटर्न आपको काट सकता है अगर आप भूल जाएँ। result.textआसान मोड है। बेहतर एक्सट्रैक्शन के लिए आपकोresult.textBlocksचाहिए होगा ताकि आप ट्रैक रख सकें कि रसीद पर कौन सी लाइनें एक-दूसरे के बगल में हैं।- ML Kit रेक्टैंगल भी लौटाता है। ये ओवरले खींचने (“हमने इसे डिटेक्ट किया”) और यह जाँचने के लिए उपयोगी हैं कि यूज़र ने सचमुच बिल को फ़्रेम किया या नहीं।
फ़ील्ड एक्सट्रैक्शन: कच्चे टेक्स्ट से स्ट्रक्चर्ड डेटा तक
OCR आपको टेक्स्ट देता है। दिलचस्प काम उस टेक्स्ट को एक सब्सक्रिप्शन में बदलना है। Subly के लिए, इसका मतलब है चार फ़ील्ड्स:
- सर्विस नाम — “Netflix”, “Spotify”, “ChatGPT Plus”
- राशि — “₺199.00” या “$9.99”
- बिलिंग फ़्रीक्वेंसी — मासिक, वार्षिक
- अगली चार्ज तारीख — भविष्य में पड़ने वाली तारीख़
एक व्यावहारिक ऑन-डिवाइस पाइपलाइन ऐसी दिखती है:
data class ExtractedSubscription(
val service: String?,
val amount: Money?,
val cycle: BillingCycle?,
val nextDate: LocalDate?,
)
fun extract(text: String): ExtractedSubscription {
val lines = text.lines().map { it.trim() }.filter { it.isNotEmpty() }
return ExtractedSubscription(
service = findService(lines),
amount = findAmount(lines),
cycle = findCycle(lines),
nextDate = findNextDate(lines),
)
}
हर find* फ़ंक्शन एक छोटी, टेस्ट करने योग्य चीज़ है। असली इम्प्लीमेंटेशन से कुछ सबक:
- सर्विस नाम सबसे कठिन है। आम सर्विसेज़ की एक डिक्शनरी (Netflix, Spotify, ChatGPT, YouTube Premium, Apple Music…) 80% मामलों को पकड़ लेती है। बाक़ी के लिए, रसीद की पहली प्रमुख लाइन या इनवॉइस ईमेल में
From:/Sent by:हेडर के इर्द-गिर्द एक ह्यूरिस्टिक अच्छा काम करता है। - राशि सबसे आसान है। एक लोकेल-अवेयर रेगेक्स जैसे
(?:[₺$€£])\s?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?ज़्यादातर मुद्राओं को कवर कर लेता है। पार्स करने से पहले हमेशा हज़ार के सेपरेटर हटा दें। - साइकिल राशि के इर्द-गिर्द की भाषा से आती है: “per month”, “/mo”, “monthly”, “aylık”, “yearly”, “/yr”। एक छोटी Turkish + English शब्दावली आपकी ज़रूरत को पूरा कर देती है।
- अगली तारीख दो रणनीतियाँ हैं: अगर मौजूद हों तो स्पष्ट वाक्यांश उठाएँ (“next billing on”, “renews on”), वरना रसीद पर सबसे नई तारीख़ में एक साइकिल जोड़कर अनुमान लगाएँ।
इसमें से कुछ भी शानदार नहीं है। यह सब डिवाइस पर है, इसलिए किसी भी विफलता का मतलब लीक हुआ इनवॉइस नहीं है।
यूज़र को काम दिखाएँ
एक स्कैनर एक ब्लैक बॉक्स है अगर आप यह न दिखाएँ कि उसने क्या किया। Subly का पैटर्न: जैसे ही कोई फ़्रेम मैच होता है, कैमरा प्रीव्यू पर एक कार्ड खींचें जिसमें एक्सट्रैक्ट किए गए फ़ील्ड्स और एक Edit बटन हो। OCR में भरोसा नाज़ुक होता है; एक Edit बटन ईमानदार और मुफ़्त है।
@Composable
fun ExtractedCard(s: ExtractedSubscription, onEdit: () -> Unit, onAccept: () -> Unit) {
Card(...) {
Text("AI detected", style = labelMono)
Text("${s.service ?: "—"} ${s.amount?.format() ?: ""}", style = title)
Text("${s.cycle?.label} · next ${s.nextDate?.format()}", style = body)
Row {
TextButton(onClick = onEdit) { Text("Edit") }
FilledButton(onClick = onAccept) { Text("Add") }
}
}
}
अगर चार में से दो फ़ील्ड ग़ायब हों, तो अपने आप न जोड़ें — यूज़र को उस मैनुअल फ़ॉर्म में ले जाएँ जो आपने जो हासिल किया उससे पहले से भरा हो।
ऑन-डिवाइस OCR से आपको क्या नहीं मिलता
कुछ ईमानदार ट्रेड-ऑफ़:
- भाषाएँ। हर स्क्रिप्ट को अपने मॉडल की ज़रूरत होती है। Latin डिफ़ॉल्ट रूप से बंडल है; Chinese, Japanese, Korean, और Devanagari अलग हैं।
- टेबलनुमा लेआउट। अगर आपको मल्टी-कॉलम इनवॉइस से सटीक कॉलम-रो पुनर्निर्माण चाहिए, तो आप अपनी अपेक्षा से ज़्यादा लेआउट कोड लिखेंगे। ज़्यादातर सब्सक्रिप्शन रसीदों के लिए यह कोई समस्या नहीं है।
- हस्तलेखन। इसका वादा न करें। यहाँ तो क्लाउड OCRs भी अविश्वसनीय हैं।
- मॉडल का आकार। कई भाषा मॉडल जोड़ने से आपका APK काफ़ी बढ़ सकता है। अगर आपको इंस्टॉल साइज़ की परवाह है तो Play के App Bundle और फ़ीचर डिलीवरी का उपयोग करें।
मार्केटिंग लाइन खुद-ब-खुद लिख जाती है
एक लाइन जो मैं अपनी प्राइवेसी पॉलिसी में कह पाना चाहता हूँ: “जो बिल इमेज आप कैप्चर करते हैं, वह हमारे सर्वर पर अपलोड नहीं होती और प्रोसेसिंग के बाद रखी नहीं जाती।” ऑन-डिवाइस ML Kit के साथ, वह लाइन शाब्दिक रूप से यही है कि कोड कैसे काम करता है। इसमें कुछ भी मार्केटिंग नहीं है — यह एक आर्किटेक्चर स्टेटमेंट है, और प्राइवेसी पॉलिसी उसी से निकलती है, न कि उल्टा।
2026 में ऑन-डिवाइस OCR के लिए यही असली तर्क है। यह सिर्फ़ थोड़ा तेज़ या थोड़ा सस्ता नहीं है। बात यह है कि “हम आपका डेटा कभी नहीं देखते” एक ऐसी बात है जो आप कह सकते हैं क्योंकि यह सच है, इसलिए नहीं कि लीगल ने कहा कि शायद ठीक है।
अगर आप सब्सक्रिप्शन / रसीद / व्यक्तिगत-वित्त के क्षेत्र में कुछ बना रहे हैं और आप निश्चित नहीं हैं कि किस रास्ते जाएँ, तो डिफ़ॉल्ट रूप से ऑन-डिवाइस चुनें। लाइब्रेरी मुफ़्त है, प्राइवेसी की कहानी पुख़्ता है, और सबसे बुरा जो हो सकता है वह यह है कि आप एक ऐसा फ़ीचर शिप करें जिसकी बराबरी आपके प्रतिस्पर्धी अपना स्टैक फिर से बनाए बिना शाब्दिक रूप से नहीं कर सकते।
Subly अब Google Play पर लाइव है — प्राइवेसी-फर्स्ट आर्किटेक्चर से, वादे से नहीं। इसके बारे में ऐप पेज पर और जानें।
// संबंधित पठन
जर्नल से और भी
Granyn बनाना: बिना बैंक लॉगिन वाला बजट ट्रैकर, बस तीन टेबल में
Granyn एक भी बैंक अकाउंट जोड़े बिना कई मुद्राओं में खर्च कैसे ट्रैक करता है और आवर्ती बिलों को कैसे पकड़ता है — इसके पीछे का Room स्कीमा, और इससे जुड़े समझौते।
Stocky बनाना: आपकी रसोई में क्या खत्म होने वाला है, इसका पूर्वानुमान — वह भी डिवाइस पर ही
Stocky एक बारकोड स्कैन और थोड़े-से उपयोग इतिहास को ऐसी शॉपिंग लिस्ट में कैसे बदल देता है जो आपके खत्म होने से पहले ही सामने आ जाती है — और सारा गणित, सारा डेटा, आपके फोन पर ही रहता है।
2026 में लोकल-फर्स्ट Android: SQLite, Room, और यूज़र डेटा को डिवाइस पर ही रखना
Room और SQLite के साथ लोकल-फर्स्ट Android ऐप बनाने की 2026 गाइड — स्कीमा डिज़ाइन, माइग्रेशन, WAL, एक्सपोर्ट, और सिंक कब जोड़ें (और कब नहीं)।