OCR respectueux de la vie privée sur Android : comment Subly lit les factures sans le cloud
Un guide pratique 2026 de l'OCR sur l'appareil sous Android avec ML Kit Text Recognition — la configuration, le code et les astuces d'extraction de champs qui gardent les données utilisateur hors de vos serveurs.
Au moment où un utilisateur pointe son téléphone vers une facture, votre application a un choix à faire. Vous pouvez envoyer cette image à un service OCR dans le cloud et récupérer un texte propre en 400 ms — au prix de faire transiter le document financier privé de quelqu’un par vos serveurs et un tiers. Ou vous pouvez faire le travail sur l’appareil, rédiger une politique de confidentialité plus honnête, et ne jamais avoir à craindre qu’une nouvelle fuite de données soit de votre faute.
J’ai choisi la seconde voie pour Subly, un suivi d’abonnements Android qui vous permet de scanner une facture Netflix ou Spotify et de faire remplir automatiquement le nom du service, le montant, la fréquence et la date du prochain prélèvement. Rien ne quitte le téléphone. Cet article explique comment cela fonctionne en 2026 — la configuration, le code et les astuces d’extraction de champs que j’ai apprises à la dure.
Pourquoi sur l’appareil, alors que le cloud est si facile
Les API OCR dans le cloud tiennent en une ligne. Google Cloud Vision, AWS Textract, Azure Read — elles vous donnent toutes des résultats nets et veulent toutes les images de vos utilisateurs. Pour un suivi d’abonnements, ces images peuvent inclure :
- Le nom, les 4 derniers chiffres ou l’adresse e-mail complète du titulaire du compte
- Des blocs d’adresse en bas d’une facture imprimée
- Les détails du moyen de paiement
- Le prix complet, les postes de dépense et les dates
Rien dans cette liste n’est quelque chose que je souhaite voir figurer dans les journaux d’un tiers. Et du point de vue produit, « nous n’envoyons jamais vos factures nulle part » est un argument marketing bien plus fort que « nous envoyons vos factures à Google Cloud et les supprimons sous 30 jours ». L’architecture suit donc la valeur : garder l’image sur l’appareil, ne persister que les champs extraits, et ne pas ouvrir de connexion internet du tout pour le scan.
L’octet le plus privé est celui qui ne quitte jamais le téléphone.
ML Kit Text Recognition v2 : la bête de somme
ML Kit Text Recognition v2 de Google est, en 2026, la réponse ennuyeuse mais correcte pour l’OCR sur l’appareil sous Android. Il embarque le modèle latin intégré à votre application (environ 4 Mo), s’exécute sur un téléphone de classe Pixel en quelques dizaines de millisecondes, et gère assez bien le texte imprimé pour les reçus, factures et captures d’écran d’e-mails.
Ce que j’apprécie :
- Aucune permission réseau requise. Vous pouvez livrer une application avec
INTERNETretiré et l’OCR fonctionne quand même. À lui seul, cela vaut le temps d’intégration. - Fonctionne hors ligne. Les scans en mode avion sont une fonctionnalité, pas un bug.
- Modèle par application. Intégrer le modèle signifie aucun téléchargement surprise au premier lancement et aucune boîte de dialogue
Try Againquand le Wi-Fi de l’utilisateur est capricieux. - Gratuit. Aucun coût d’API à mesure que vous montez en charge. Pour une application indie, c’est la différence entre lancer et trop réfléchir.
Le compromis porte sur la précision dans les cas limites : l’écriture manuscrite est aléatoire, les reçus denses imprimés en thermique peuvent brouiller la mise en page, et les écritures non latines nécessitent un modèle différent. Pour le cas d’usage reçu/facture/e-mail, c’est largement suffisant.
Configuration en 2026
La dépendance 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")
}
Et une ligne dans votre manifeste pour que Play reconnaisse que le modèle est intégré et téléchargé à l’avance :
<application>
<meta-data
android:name="com.google.mlkit.vision.DEPENDENCIES"
android:value="ocr" />
</application>
Voilà toute la configuration. Aucune clé d’API, aucun JSON de compte de service, aucune requête signée.
Reconnaître le texte à partir d’une image CameraX
Voici la boucle centrale. CameraX vous fournit des images ImageProxy, vous en enveloppez une dans un InputImage, et le module de reconnaissance vous renvoie un objet Text avec paragraphes, lignes et rectangles.
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() }
}
Quelques points à connaître :
- Appelez toujours
image.close()exactement une fois, même en cas d’erreur. Le modèle de listener de ML Kit peut vous jouer des tours si vous l’oubliez. result.textest le mode facile. Pour une meilleure extraction, vous voudrezresult.textBlocksafin de suivre quelles lignes sont adjacentes les unes aux autres sur le reçu.- ML Kit renvoie aussi des rectangles. Ils sont utiles pour dessiner des surcouches (« nous avons détecté ceci ») et pour vérifier que l’utilisateur a bien cadré la facture.
Extraction de champs : du texte brut aux données structurées
L’OCR vous donne du texte. Le travail intéressant consiste à transformer ce texte en abonnement. Pour Subly, cela signifie quatre champs :
- Nom du service — « Netflix », « Spotify », « ChatGPT Plus »
- Montant — « ₺199.00 » ou « $9.99 »
- Fréquence de facturation — mensuelle, annuelle
- Date du prochain prélèvement — la date dans le futur
Un pipeline pragmatique sur l’appareil ressemble à ceci :
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),
)
}
Chaque fonction find* est une petite chose testable. Quelques leçons tirées de l’implémentation réelle :
- Le nom du service est le plus difficile. Un dictionnaire de services courants (Netflix, Spotify, ChatGPT, YouTube Premium, Apple Music…) couvre 80 % des cas. Pour le reste, la première ligne proéminente du reçu ou une heuristique autour de l’en-tête
From:/Sent by:dans les e-mails de facturation fonctionne bien. - Le montant est le plus facile. Une expression régulière tenant compte de la locale comme
(?:[₺$€£])\s?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{2})?couvre la plupart des devises. Supprimez toujours les séparateurs de milliers avant l’analyse. - Le cycle se déduit du vocabulaire autour du montant : « per month », « /mo », « monthly », « aylık », « yearly », « /yr ». Un petit vocabulaire turc + anglais couvre ce dont vous avez besoin.
- La prochaine date repose sur deux stratégies : repérer les formulations explicites (« next billing on », « renews on ») si elles sont présentes, sinon déduire à partir de la date la plus récente du reçu plus un cycle.
Rien de tout cela n’est glorieux. Tout se passe sur l’appareil, donc aucun échec ne signifie une facture divulguée.
Montrez le travail à l’utilisateur
Un scanner est une boîte noire si vous ne montrez pas ce qu’il a fait. Le modèle de Subly : dès qu’une image correspond, dessinez une carte par-dessus l’aperçu de la caméra avec les champs extraits et un bouton Edit. La confiance dans l’OCR est fragile ; un bouton Edit est honnête et gratuit.
@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") }
}
}
}
Si deux des quatre champs manquent, n’ajoutez pas automatiquement — dirigez l’utilisateur vers le formulaire manuel pré-rempli avec ce que vous avez pu obtenir.
Ce que l’OCR sur l’appareil ne vous donne pas
Quelques compromis honnêtes :
- Langues. Chaque écriture nécessite son propre modèle. Le latin est intégré par défaut ; le chinois, le japonais, le coréen et la devanagari sont séparés.
- Mises en page tabulaires. Si vous avez besoin d’une reconstruction exacte colonnes-lignes à partir d’une facture multicolonne, vous écrirez plus de code de mise en page que prévu. Pour la plupart des reçus d’abonnement, ce n’est pas un problème.
- Écriture manuscrite. Ne la promettez pas. Même les OCR dans le cloud sont peu fiables ici.
- Taille du modèle. Ajouter plusieurs modèles de langue peut faire grossir votre APK de manière notable. Utilisez l’App Bundle de Play et la livraison de fonctionnalités si la taille d’installation vous importe.
L’argument marketing s’écrit tout seul
Une phrase que je veux pouvoir écrire dans notre politique de confidentialité : « l’image de la facture que vous capturez n’est pas téléversée vers nos serveurs et n’est pas conservée après traitement ». Avec ML Kit sur l’appareil, cette phrase décrit littéralement le fonctionnement du code. Rien là-dedans n’est du marketing — c’est une déclaration d’architecture, et la politique de confidentialité en découle plutôt que l’inverse.
C’est le véritable argument en faveur de l’OCR sur l’appareil en 2026. Ce n’est pas seulement un peu plus rapide ou un peu moins cher. C’est que « nous ne voyons jamais vos données » est une chose que vous pouvez dire parce que c’est vrai, et non parce que le juridique a estimé que c’était probablement acceptable.
Si vous construisez quoi que ce soit dans le domaine des abonnements / reçus / finances personnelles et que vous ne savez pas quelle direction prendre, optez par défaut pour le sur-appareil. La bibliothèque est gratuite, l’argument de confidentialité est à toute épreuve, et le pire qui puisse arriver est que vous livriez une fonctionnalité que vos concurrents ne peuvent littéralement pas égaler sans reconstruire leur stack.
Subly est maintenant disponible sur Google Play — respectueux de la vie privée par architecture, pas par promesse. Plus d’informations sur la page de l’application.
// À lire aussi
D’autres notes du journal
Construire Granyn : un suivi de budget sans connexion bancaire, en trois tables
Comment Granyn suit les dépenses dans plusieurs devises et détecte les factures récurrentes sans lier le moindre compte bancaire — le schéma Room derrière tout ça, et les compromis qu'il impose.
Construire Stocky : prédire ce qui va manquer dans votre cuisine — sur l'appareil lui-même
Comment Stocky transforme un scan de code-barres et un peu d'historique d'utilisation en une liste de courses qui apparaît avant que vous ne tombiez à court — avec tous les calculs, et toutes les données, qui restent sur votre téléphone.
Android local-first en 2026 : SQLite, Room et garder les données de l'utilisateur sur l'appareil
Un guide 2026 pour construire des applications Android local-first avec Room et SQLite — conception du schéma, migrations, WAL, exports, et quand (et quand ne pas) ajouter la synchronisation.