ocr-carte-visite-vcf.md 74719 octets

Plan technique OCR carte de visite vers VCF

Statut : plan d'implémentation — itération documentaire
Dernière mise à jour : 2026-07-22
Décision retenue : CameraX + OpenCV + Tesseract4Android + Luciole-1B
Philosophie : produit fonctionnel complet — pas de MVP, pas de livraison partielle

Sommaire

  1. Objectif
  2. Philosophie produit
  3. Contexte du dépôt
  4. Contrainte principale
  5. Workflow cible
  6. Choix techniques retenus
  7. Problème qualité OCR
  8. Recherche technique des solutions
  9. Matrice de décision
  10. Stratégie qualité OCR
  11. Interfaces techniques
  12. Tickets commit par commit
  13. User stories
  14. Recommandation finale
  15. Sources

Objectif

Ajouter dans Luciole une fonctionnalité de scan de carte de visite qui reste locale sur le téléphone, réutilise Luciole-1B pour la structuration sémantique, puis permet :

  • le préremplissage d'un contact Android
  • l'export d'un fichier .vcf

Philosophie produit — pas de MVP

Ce plan vise un produit fonctionnel complet, pas une démo partielle ni un prototype jetable.

Principes directeurs

  1. Pas de livraison à moitié codée : chaque brique livrée est réellement implémentée, testée et intégrée.
  2. Pas de raccourcis en production : aucun moteur factice, aucun TODO bloquant dans le chemin utilisateur, aucune fausse sortie contact.
  3. Revue régulière obligatoire : chaque étape se termine par une porte de qualité (tests verts + vérification manuelle) avant de passer à la suivante.
  4. Tests d'abord sur la logique critique : parseurs, scan, OCR, structuration, intents — pas seulement de l'UI.
  5. Périmètre complet dès le départ : scan dédié, chat, brouillon, Contacts Android, export VCF, benchmark, documentation.

Anti-patterns interdits

InterditPourquoi
FakeScanEngine / FakeOcrEngine en prodmasque des lacunes d'implémentation
retourner un contact hardcodé « pour tester »fausse la chaîne complète
reporter l'intégration chat « à plus tard »livre un flux incomplet
livrer Contacts sans VCF (ou l'inverse)fonctionnalité métier inachevée
commit WIP ou fix laterdette immédiate, perte de temps ensuite
dépendance Google Play « temporaire »contredit la contrainte produit

Note tests unitaires : MockWebServer (déjà utilisé dans le dépôt) est autorisé uniquement pour tester CerveauServeur hors appareil. Ce n'est pas un raccourci produit.

Portes de qualité entre étapes

PorteAprès ticketsCritère de passage
R1C01-C08contrat ContactCard + VCF validés par tests, schéma JSON figé
R2C09-C18pipeline scan+OCR réel sur corpus, pas de stub, ./gradlew test vert
R3C19-C22extractContact() fonctionne avec serveur local, non-régression 11 actions
R4C23-C30parcours UI complet capture → brouillon éditable, testé sur appareil
R5C31-C40Contacts + VCF + déclenchement chat opérationnels
R6C41-C43benchmark documenté, erreurs couvertes, README à jour

Règle : ne pas ouvrir l'étape N+1 tant que la porte R(N) n'est pas validée.

Contexte du dépôt

Après lecture de la structure du projet et du README.md, les points suivants sont établis :

  • le modèle actuel est Luciole-1B appelé via llama.cpp sur 127.0.0.1:8080
  • l'application Android est organisée autour de cerveau, mains, model, ui
  • le cerveau actuel ne traite que du texte et renvoie un JSON d'action
  • il n'existe aujourd'hui ni OCR, ni pipeline image, ni vCard/VCF, ni insertion de contact Android

Arborescence utile

  • README.md
  • android/app/src/main/java/fr/openllm/luciole/cerveau/Cerveau.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/CerveauServeur.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/SystemPrompt.kt
  • android/app/src/main/java/fr/openllm/luciole/model/Action.kt
  • android/app/src/main/java/fr/openllm/luciole/model/ActionJson.kt
  • android/app/src/main/java/fr/openllm/luciole/mains/Mains.kt
  • android/app/src/main/java/fr/openllm/luciole/mains/Contacts.kt
  • android/app/src/main/java/fr/openllm/luciole/MainActivity.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/LucioleApp.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ChatScreen.kt
  • android/app/src/main/AndroidManifest.xml
  • android/app/build.gradle.kts
  • contract/actions.schema.json

Contrainte principale

Luciole-1B dans ce dépôt est un modèle texte, pas un modèle vision. Il ne peut donc pas faire seul un OCR image réel dans l'application telle qu'elle existe aujourd'hui.

La conséquence est simple :

  • non faisable en l'état : image -> Luciole seul -> OCR
  • faisable réalistement : image -> moteur OCR local -> texte -> Luciole -> contact structuré

Workflow cible recommandé

Le workflow cible recommandé est :

Image -> scan document local -> correction perspective/rotation -> OCR local -> texte brut -> Luciole -> JSON contact structuré -> écran de validation -> insertion Contacts Android / export VCF

Pourquoi ne pas réutiliser le JSON d'action actuel

Le JSON d'action existant dans le projet sert à représenter des intentions Android fermées, par exemple :

  • appel
  • agenda
  • message
  • ouvrir

Une carte de visite ne produit pas naturellement une action ; elle produit un jeu de données structurées. Il faut donc créer un contrat distinct pour les contacts au lieu de forcer ces données dans Action.kt.

Contrat métier cible

Le nouvel objet métier conseillé est un ContactCard, avec un JSON cible de ce type :

{
  "full_name": "Jean Dupont",
  "first_name": "Jean",
  "last_name": "Dupont",
  "company": "Acme",
  "job_title": "Directeur commercial",
  "phones": ["+33612345678"],
  "emails": ["jean.dupont@acme.fr"],
  "website": "https://acme.fr",
  "address": "12 rue Exemple, 75000 Paris",
  "note": "Informations ambiguës ou texte résiduel"
}

Architecture cible

flowchart TD
    user[Utilisateur] --> scanFlow[ScanCarteScreen]
    user --> chatFlow[ChatLuciole]
    scanFlow --> scanEngine[ScanDocumentLocal]
    scanEngine --> correctedImage[ImageCorrigee]
    correctedImage --> ocrEngine[OcrLocal]
    ocrEngine --> rawText[TexteOCR]
    chatFlow --> intentRouter[CerveauAction]
    intentRouter --> openScan[ActionScannerCarte]
    rawText --> contactPrompt[PromptStructurationContact]
    contactPrompt --> luciole[Luciole1B]
    luciole --> contactJson[ContactCardJson]
    contactJson --> draft[ContactDraft]
    draft --> review[ValidationUtilisateur]
    review --> contactInsert[InsertionContactsAndroid]
    review --> vcfExport[ExportVCF]

Choix techniques retenus

Cette section fixe les décisions d'implémentation pour Luciole, avec un comparatif par couche afin d'expliquer pourquoi chaque choix a été retenu.

Contrainte directrice

Le plan vise une indépendance réelle :

  • Android générique
  • sans Google Play Services
  • sans SDK propriétaire obligatoire
  • traitement local sur l'appareil

Il n'y a donc pas de variante Android standard dans ce plan.

Pile retenue

CameraX
-> OpenCvScanEngine
-> TesseractOcrEngine
-> OcrPostProcessor
-> Luciole-1B
-> ContactCard
-> validation utilisateur
-> Contacts Android / export VCF

Comparatif par couche

1. Capture caméra

OptionSans Play ServicesOpen sourceMaturitéComplexitéDécision
CameraXouiouiexcellentemoyenneretenu
Camera2 API directeouiouibonneélevéerejeté
Intent système IMAGE_CAPTUREouiouibonnefaiblerejeté

Pourquoi CameraX

  • API Android moderne et maintenue
  • bonne intégration avec Compose et ActivityResult
  • pas de dépendance Google Play
  • plus simple et plus robuste qu'une implémentation Camera2 maison

Rejet des alternatives

  • Camera2 : plus de code bas niveau, plus fragile
  • intent système : moins de contrôle sur la qualité de capture et le flux UX

2. Scan documentaire / correction géométrique

OptionSans Play ServicesOpen sourcePerspective / deskewLicenceMaturitéDécision
OpenCV custom (OpenCvScanEngine)ouiouiouiApache 2.0bonneretenu
AndroidDocumentScannerouiouiouiMITmoyenneinspiration / fallback
OpenNoteScanner dérivéouiouiouiGPLv3bonnerejeté
trudido-scannerouiouiouiGPLv3faiblerejeté
ML Kit Document Scannernonnonouipropriétaireexcellentehors cible

Pourquoi OpenCV en implémentation maison

  • seule brique réellement indépendante et maîtrisable
  • couvre le besoin critique : détection des bords, homographie, crop, rotation, deskew
  • s'intègre proprement derrière ScanEngine
  • évite une dépendance GPL ou un projet trop jeune

Rôle d'AndroidDocumentScanner

  • ne pas l'intégrer directement dans la première itération si OpenCvScanEngine converge assez vite
  • s'en servir comme référence d'implémentation si OpenCvScanEngine tarde à converger
  • licence MIT plus souple si un jour on veut factoriser la détection de document

Rejet des alternatives

  • OpenNoteScanner / trudido-scanner : utiles en référence, mais GPL ou maturité insuffisante pour une intégration produit directe
  • ML Kit : hors cible car dépendant de Google Play Services

3. OCR

OptionSans Play ServicesOpen sourceQualité OCRTaille / perfLicenceDécision
Tesseract4AndroidouiouicorrectebonneApache 2.0retenu
PaddleOCR Androidouiouimeilleureplus lourdeApache 2.0plan B
OpenCV seulouiouinonrejeté
ML Kit Text Recognitionnonnonbonnebonnepropriétairehors cible

Pourquoi Tesseract4Android

  • wrapper Android moderne autour de Tesseract 5.x
  • licence Apache 2.0
  • intégration simple dans une app Kotlin
  • suffisant pour des cartes de visite, surtout si l'image est déjà corrigée en amont
  • cohérent avec l'architecture OcrEngine

Pourquoi pas PaddleOCR en premier choix

  • meilleure qualité OCR potentielle
  • mais intégration plus lourde
  • modèles ONNX plus volumineux
  • ne résout pas à lui seul le problème de perspective / angle

Plan B

  • si le benchmark montre que Tesseract est insuffisant sur le corpus réel, remplacer uniquement TesseractOcrEngine par PaddleOcrEngine

4. Structuration sémantique

OptionRôleDécision
Luciole-1Btransformer le texte OCR en ContactCardretenu
Regex / heuristiques seulesextraire téléphone, e-mail, URLcomplément obligatoire
JSON d'action existantrouter des intents Androidrejeté pour ce flux

Pourquoi Luciole-1B

  • déjà présent dans l'application
  • bon pour distinguer nom, poste, société, adresse à partir d'un texte bruité
  • cohérent avec la promesse produit

Pourquoi pas le JSON d'action actuel

  • le contrat actuel sert à piloter des actions Android, pas à représenter une fiche contact

5. Sortie contact

OptionSans Play ServicesComplexitéDécision
Préremplissage Intent.ACTION_INSERT Contactsouifaibleretenu
Export .vcf localouimoyenneretenu
Création contact silencieuse via ContentProviderouiélevéerejeté (validation utilisateur obligatoire)

Pourquoi ce choix

  • respecte le modèle de sécurité déjà visible dans Mains.kt
  • l'utilisateur valide avant insertion
  • Contacts et VCF sont tous deux livrés dans le même périmètre produit (ordre d'implémentation C31 puis C33, pas de report fonctionnel)

Décision finale synthétique

CoucheChoix retenuAlternative écartéeRaison principale
CaptureCameraXCamera2 / intent systèmemodernité et robustesse
Scan / géométrieOpenCvScanEngineML Kit, Scanbot, Dynamsoftindépendance et contrôle
OCRTesseract4AndroidPaddleOCR (plan B), ML Kitsimplicité + licence + offline
StructurationLuciole-1Bheuristiques seulessémantique contact
ComplémentOcrPostProcessor + regexfiabiliser tel/email/url
SortieContacts puis VCFinsertion silencieusevalidation utilisateur

Dépendances Gradle cibles

// Capture
implementation("androidx.camera:camera-camera2:1.4.1")
implementation("androidx.camera:camera-lifecycle:1.4.1")
implementation("androidx.camera:camera-view:1.4.1")

// Vision / scan documentaire
implementation("org.opencv:opencv:4.10.0")

// OCR
implementation("cz.adaptech.tesseract4android:tesseract4android-openmp:4.9.0")

Notes :

  • les versions exactes seront figées au moment de l'implémentation
  • les traineddata Tesseract (fra, eng) seront embarqués ou installés localement par l'app
  • aucune dépendance com.google.android.gms ni com.google.mlkit ne doit être ajoutée

Modules Kotlin à créer en priorité

ModuleRôle
scan/ScanEngine.ktcontrat scan documentaire
scan/OpenCvScanEngine.ktdétection bords, homographie, deskew
ocr/OcrEngine.ktcontrat OCR
ocr/TesseractOcrEngine.ktOCR local
ocr/OcrPostProcessor.kttri des lignes, regex, confiance
contact/ContactCard.ktmodèle métier
contact/ContactCardJson.ktparseur JSON contact
cerveau/ContactPrompt.ktprompt Luciole dédié
ui/ScanCarteScreen.ktparcours utilisateur
ui/ContactDraftScreen.ktvalidation / correction

Ordre d'implémentation technique

  1. ContactCard + VCardSerializer
  2. OpenCvScanEngine avec un corpus d'images de test
  3. TesseractOcrEngine
  4. OcrPostProcessor
  5. Cerveau.extractContact()
  6. écran scan + brouillon contact
  7. insertion Contacts Android
  8. export VCF
  9. déclenchement depuis le chat

Plan d'implémentation détaillé

Cette section transforme le plan technique en feuille de route exécutable, alignée sur l'architecture décrite dans le README.md :

  • cerveau pour l'interprétation
  • mains pour les sorties déterministes
  • ui pour le parcours utilisateur

Principe de mise en oeuvre :

  • construire d'abord le contrat métier
  • brancher ensuite le pipeline scan + OCR
  • raccorder après cela le cerveau
  • finir par la sortie utilisateur et l'intégration chat

Phase 0 - Préparation du chantier

But

Stabiliser le périmètre avant toute ligne de code applicatif.

Travaux

  1. Valider les dépendances autorisées :
    • CameraX
    • OpenCV
    • Tesseract4Android
  2. Préparer un petit corpus local de cartes de visite de test :
    • droite
    • inclinée
    • ombrée
    • graphique
  3. Fixer le contrat ContactCard et le format VCF cible.
  4. Lister les nouvelles permissions ou capacités Android réellement nécessaires.

Livrables

  • décision technique gelée
  • corpus de test minimal
  • contrat JSON contact stabilisé

Risque à lever

  • coût d'intégration natif de OpenCV
  • stratégie d'embarquement des traineddata Tesseract

Phase 1 - Fondations métier et contrats

Stories couvertes

  • Story 0
  • Story 1

Objectif

Créer les objets métier et les parseurs avant toute intégration caméra ou OCR.

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/contact/ContactCard.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/ContactCardJson.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/VCardSerializer.kt

Tests à créer

  • android/app/src/test/java/fr/openllm/luciole/contact/ContactCardTest.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/ContactCardJsonTest.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/VCardSerializerTest.kt

Tâches détaillées

  1. Définir ContactCard avec champs simples et listes :
    • fullName
    • firstName
    • lastName
    • company
    • jobTitle
    • phones
    • emails
    • website
    • address
    • note
  2. Définir la politique de champs facultatifs :
    • null pour l'absence
    • listes vides pour multi-valeurs absentes
  3. Créer ContactCardJson :
    • parser tolérant
    • normalisation minimale
    • rejet propre des JSON invalides
  4. Créer VCardSerializer :
    • VCARD 3.0
    • échappement des séparateurs
    • multi-téléphones / multi-e-mails

Critères de validation

  • le contrat contact est indépendant de Action.kt
  • le VCF produit est importable par Contacts Android
  • les tests couvrent caractères accentués, valeurs partielles et multi-champs

Phase 2 - Pipeline image local

Stories couvertes

  • Story 2

Objectif

Obtenir un texte OCR exploitable localement à partir d'une photo de carte.

Fichiers à modifier

  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/scan/ScanEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/scan/OpenCvScanEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrResult.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/TesseractOcrEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrPostProcessor.kt

Tests à créer

  • android/app/src/test/java/fr/openllm/luciole/scan/OpenCvScanEngineTest.kt
  • android/app/src/test/java/fr/openllm/luciole/ocr/OcrPostProcessingTest.kt

Tâches détaillées

  1. Ajouter les dépendances Gradle et vérifier leur compatibilité avec minSdk 31.
  2. Définir ScanEngine avec une sortie explicite :
    • image corrigée
    • angle retenu
    • score ou drapeau de confiance
  3. Implémenter OpenCvScanEngine :
    • conversion bitmap/mat
    • détection du contour principal
    • approximation quadrilatère
    • homographie
    • rotation / deskew
  4. Définir OcrEngine et OcrResult.
  5. Implémenter TesseractOcrEngine :
    • initialisation moteur
    • sélection des langues
    • OCR bitmap local
  6. Implémenter OcrPostProcessor :
    • nettoyage des lignes
    • extraction regex téléphone / e-mail / URL
    • fusion des lignes proches si utile
  7. Définir les erreurs de pipeline :
    • document non détecté
    • OCR vide
    • modèles de langue absents

Critères de validation

  • le pipeline fonctionne sans Google Play Services
  • une carte inclinée est redressée avant OCR
  • le texte OCR est récupéré même si la structuration Luciole n'est pas encore branchée

Phase 3 - Raccordement au cerveau

Stories couvertes

  • Story 3

Objectif

Réutiliser Luciole-1B pour structurer le texte OCR, sans casser le routage des 11 actions existantes.

Fichiers à modifier

  • android/app/src/main/java/fr/openllm/luciole/cerveau/Cerveau.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/CerveauServeur.kt

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/cerveau/ContactPrompt.kt

Tests à créer

  • android/app/src/test/java/fr/openllm/luciole/cerveau/CerveauServeurContactTest.kt

Tâches détaillées

  1. Ajouter une nouvelle capacité :
    • suggest() reste inchangée pour le chat courant
    • extractContact(rawText: String) est ajoutée à côté
  2. Écrire un prompt séparé pour la structuration contact :
    • sortie JSON stricte
    • pas d'invention de champ absent
    • conservation du texte ambigu dans note
  3. Réutiliser la mécanique HTTP existante de CerveauServeur.
  4. Parser la réponse avec ContactCardJson.
  5. Définir une stratégie de fallback :
    • si JSON invalide, afficher un brouillon vide mais conserver le texte OCR

Critères de validation

  • aucune régression sur le contrat des 11 actions existantes
  • le cerveau peut structurer un texte OCR bruité en ContactCard
  • le texte brut reste disponible si Luciole échoue

Phase 4 - Parcours UI dédié

Stories couvertes

  • Story 4
  • Story 6

Objectif

Créer un parcours utilisateur clair, séparé du chat, cohérent avec la navigation actuelle observée dans LucioleApp.kt.

Fichiers à modifier

  • android/app/src/main/java/fr/openllm/luciole/ui/LucioleApp.kt
  • android/app/src/main/java/fr/openllm/luciole/MainActivity.kt
  • android/app/src/main/res/values/strings.xml
  • android/app/src/main/res/values-en/strings.xml

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/ui/ScanCarteScreen.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ScanCarteViewModel.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ContactDraftScreen.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ContactDraftFields.kt

Tâches détaillées

  1. Ajouter un nouvel état d'écran dans LucioleApp.kt.
  2. Définir le ScanCarteViewModel avec états explicites :
    • Idle
    • Capturing
    • Scanning
    • OcrRunning
    • Structuring
    • DraftReady
    • Error
  3. Construire ScanCarteScreen :
    • capture CameraX
    • aperçu de l'image
    • relance en cas d'échec
  4. Construire ContactDraftScreen :
    • champs éditables
    • texte OCR brut visible
    • boutons Créer le contact et Exporter VCF
  5. Définir les messages d'erreur UX :
    • carte mal cadrée
    • OCR vide
    • structuration partielle

Critères de validation

  • le flux complet est compréhensible sans passer par le chat
  • l'utilisateur peut corriger avant toute action de sortie
  • l'UI reste cohérente avec le style Compose existant

Phase 5 - Sorties Android et VCF

Stories couvertes

  • Story 7

Objectif

Transformer le brouillon validé en action utile pour l'utilisateur.

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/contact/ContactInsertIntent.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/VcfShare.kt

Fichiers à modifier

  • android/app/src/main/java/fr/openllm/luciole/MainActivity.kt
  • android/app/src/main/AndroidManifest.xml

Tests à créer

  • android/app/src/test/java/fr/openllm/luciole/contact/ContactInsertIntentTest.kt

Tâches détaillées

  1. Construire l'intent Intent.ACTION_INSERT prérempli.
  2. Mapper chaque champ ContactCard vers les extras Android pertinents.
  3. Générer un fichier .vcf temporaire partageable.
  4. Définir la stratégie de partage :
    • FileProvider si nécessaire
    • nom de fichier stable et lisible
  5. Garantir qu'aucune insertion silencieuse n'est faite (validation utilisateur obligatoire).

Critères de validation

  • le contact Android s'ouvre prérempli
  • le VCF est partageable et réimportable
  • la validation utilisateur reste obligatoire

Phase 6 - Intégration au chat et finitions

Stories couvertes

  • Story 5
  • Story 8

Objectif

Raccorder la nouvelle fonctionnalité à la démo existante sans brouiller la responsabilité du chat.

Fichiers à modifier

  • contract/actions.schema.json
  • android/app/src/main/java/fr/openllm/luciole/model/Action.kt
  • android/app/src/main/java/fr/openllm/luciole/model/ActionJson.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/SystemPrompt.kt
  • android/app/src/main/java/fr/openllm/luciole/mains/Mains.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ChatScreen.kt
  • README.md

Tâches détaillées

  1. Ajouter une action scanner_carte dans le contrat des actions.
  2. Faire en sorte que cette action ouvre le flux dédié sans porter les données contact.
  3. Ajouter les tests de non-régression sur le parsing d'actions.
  4. Mesurer le flux complet sur le corpus :
    • succès scan
    • précision champs
    • temps total
  5. Mettre à jour le README.md :
    • nouvelle fonctionnalité
    • limites connues
    • dépendances locales

Critères de validation

  • le chat peut déclencher le scan sans devenir un OCR conversationnel
  • les 11 actions existantes restent compatibles
  • la documentation du dépôt reflète la nouvelle architecture

Définition de terminé (produit complet)

Le chantier est terminé uniquement quand toutes les conditions suivantes sont réunies :

  1. une carte de visite peut être photographiée dans l'app
  2. l'image est corrigée localement avant OCR (scan réel, pas de placeholder)
  3. l'OCR local produit un texte brut exploitable
  4. Luciole-1B transforme ce texte en ContactCard
  5. l'utilisateur peut corriger le brouillon avec le texte OCR visible
  6. le contact peut être prérempli dans Android et exporté en .vcf
  7. le flux est déclenchable depuis l'écran dédié et depuis le chat (scanner_carte)
  8. le flux reste 100 % local et sans dépendance Google Play Services
  9. le chat existant ne régresse pas (11 actions + nouvelle action)
  10. le benchmark sur corpus est documenté et les cas d'erreur sont couverts par tests
  11. ./gradlew :app:testDebugUnitTest passe sans régression

Tickets commit par commit

Backlog atomique pour l'implémentation. Chaque ticket = un commit, un objectif, une vérification.

Tous les tickets C01 à C43 sont obligatoires pour considérer la fonctionnalité terminée. Aucun n'est optionnel ni reportable.

Convention de message (alignée sur l'historique du dépôt) :

feat(scope): description courte
test(scope): ...
docs(scope): ...
build(android): ...

Légende : verify: = commande ou contrôle manuel minimal avant de passer au ticket suivant.

Phase 0 — Cadrage (Story 0)

#CommitFichiersverify
C01docs(ocr): documenter pipeline OCR local puis structuration LucioleREADME.mdle README explique que Luciole ne fait pas l'OCR image seul
C02docs(ocr): ajouter contrat JSON ContactCard de referencecontract/contact.schema.jsonschéma JSON valide, champs alignés avec le plan

Phase 1 — Contrat contact (Story 1)

#CommitFichiersverify
C03feat(contact): ajouter data class ContactCardcontact/ContactCard.ktcompile
C04test(contact): couvrir ContactCard champs partiels et listesContactCardTest.kt./gradlew :app:testDebugUnitTest --tests '*ContactCardTest'
C05feat(contact): ajouter parseur ContactCardJsoncontact/ContactCardJson.ktcompile
C06test(contact): couvrir ContactCardJson cas valides et invalidesContactCardJsonTest.kttests verts
C07feat(contact): ajouter VCardSerializer VCARD 3.0contact/VCardSerializer.ktcompile
C08test(contact): couvrir VCardSerializer accents et multi-champsVCardSerializerTest.kttests verts, VCF importable manuellement

Phase 2 — Dépendances et permissions (Story 2, début)

#CommitFichiersverify
C09build(android): ajouter dependances CameraX OpenCV Tesseractandroid/app/build.gradle.kts./gradlew :app:assembleDebug
C10feat(android): declarer permissions camera et stockage scanAndroidManifest.xmlmanifeste valide, pas de GMS/ML Kit

Phase 3 — Scan documentaire (Story 2)

#CommitFichiersverify
C11feat(scan): ajouter interface ScanEngine et ScanResultscan/ScanEngine.ktcompile
C12feat(scan): implementer OpenCvScanEngine detection contourscan/OpenCvScanEngine.ktcompile
C13test(scan): valider redressement sur images de referenceOpenCvScanEngineTest.kt, src/test/resources/scan/*.jpgtests verts sur carte droite + inclinee
C14feat(scan): ajouter ajustement manuel des coinsOpenCvScanEngine.kt, scan/ScanCorners.ktimage corrigee visible en debug

Phase 4 — OCR local (Story 2)

#CommitFichiersverify
C15feat(ocr): ajouter interface OcrEngine et OcrResultocr/OcrEngine.kt, ocr/OcrResult.ktcompile
C16feat(ocr): implementer TesseractOcrEngine et bootstrap traineddataocr/TesseractOcrEngine.ktOCR texte sur bitmap de test
C17feat(ocr): ajouter OcrPostProcessor regex tel email urlocr/OcrPostProcessor.ktcompile
C18test(ocr): couvrir OcrPostProcessor extraction multi-champsOcrPostProcessingTest.kttests verts

Phase 5 — Structuration Luciole (Story 3)

#CommitFichiersverify
C19feat(cerveau): ajouter ContactPrompt structuration contactcerveau/ContactPrompt.ktprompt JSON strict, pas d'action melangee
C20feat(cerveau): etendre Cerveau avec extractContactcerveau/Cerveau.ktinterface compile, suggest() inchange
C21feat(cerveau): implementer extractContact dans CerveauServeurcerveau/CerveauServeur.ktappel HTTP local retourne ContactCard
C22test(cerveau): couvrir extractContact avec MockWebServerCerveauServeurContactTest.kttests verts, 11 actions existantes intactes

Phase 6 — UI scan (Story 4)

#CommitFichiersverify
C23feat(ui): ajouter ScanCarteViewModel et machine d etatsui/ScanCarteViewModel.ktetats Idle/Scanning/Ocr/Structuring/Draft/Error
C24feat(ui): ajouter ScanCarteScreen avec preview CameraXui/ScanCarteScreen.ktcapture live sur appareil
C25feat(ui): brancher pipeline scan OCR dans ScanCarteViewModelScanCarteViewModel.kttexte OCR affiche apres capture
C26feat(ui): ajouter entree navigation Scanner une carteLucioleApp.kt, strings.xml, strings-en.xmlecran accessible depuis l'app

Phase 7 — Brouillon editable (Story 6)

#CommitFichiersverify
C27feat(ui): ajouter ContactDraftFields composablesui/ContactDraftFields.ktchamps editables nom tel email societe
C28feat(ui): ajouter ContactDraftScreen avec texte OCR brutui/ContactDraftScreen.ktcorrection manuelle possible
C29feat(ui): fusionner sortie Luciole et regex dans le brouillonScanCarteViewModel.kttel/email regex presents meme si LLM partiel
C30test(contact): couvrir fusion brouillon OCR regex LLMContactDraftMergeTest.kttests verts

Phase 8 — Sorties contact (Story 7)

#CommitFichiersverify
C31feat(contact): mapper ContactCard vers Intent ACTION_INSERTcontact/ContactInsertIntent.ktintent pre-rempli
C32test(contact): couvrir extras ContactInsertIntentContactInsertIntentTest.kttests verts
C33feat(contact): ajouter VcfShare generation fichier vcfcontact/VcfShare.ktfichier .vcf genere
C34feat(android): configurer FileProvider pour partage VCFAndroidManifest.xml, res/xml/file_paths.xml, MainActivity.ktpartage .vcf vers autre app

Phase 9 — Integration chat (Story 5)

#CommitFichiersverify
C35feat(model): ajouter action scanner_carte au schemacontract/actions.schema.json, contract/actions.gbnfschema + grammaire alignes
C36feat(model): propager scanner_carte dans Action et ActionJsonmodel/Action.kt, model/ActionJson.ktcompile
C37feat(cerveau): reconnaitre scanner_carte dans SystemPromptcerveau/SystemPrompt.ktprompt route vers action dediee
C38feat(mains): router scanner_carte vers ouverture flux scanmains/Mains.ktnouvelle Sortie ou signal navigation
C39feat(ui): ouvrir ScanCarteScreen depuis le chatChatScreen.kt, MainActivity.kt, LucioleApp.ktphrase « scanner une carte » ouvre le flux
C40test(model): couvrir parsing action scanner_carteActionScanTest.kt, MainsScanTest.kttests verts, non-regression 11 actions

Phase 10 — Qualite et documentation (Story 8)

#CommitFichiersverify
C41test(ocr): ajouter harness benchmark corpus cartesOcrBenchmarkTest.kt, src/test/resources/cards/metriques scan/OCR mesurables
C42test(android): couvrir erreurs pipeline scan OCR videtests cibles scan/ocr/uicas echec documentes
C43docs(readme): documenter scan carte visite et limites connuesREADME.mdfeature visible, contraintes offline

Étapes de revue (remplace les « jalons démo »)

ÉtapeTicketsPorteLivrable validé
E1 — ContratC01-C08R1ContactCard + VCF + schéma JSON
E2 — PipelineC09-C18R2scan + OCR réels sur corpus
E3 — CerveauC19-C22R3structuration Luciole + non-régression
E4 — Parcours UIC23-C30R4capture → brouillon éditable sur appareil
E5 — SortiesC31-C34Contacts Android + export VCF
E6 — ChatC35-C40R5scanner_carte + flux complet depuis le chat
E7 — QualitéC41-C43R6benchmark, erreurs, README

Règles de découpage commit

  1. Un commit = une responsabilité : pas de mélange parseur + UI + manifest.
  2. Tests dans le commit suivant ou le même si le commit ajoute une logique testable.
  3. Pas de commit « WIP » : chaque commit compile et les tests ajoutés passent.
  4. Pas de dépendance Google dans un commit, même transitoire.
  5. Pas d'implémentation factice : si une brique n'est pas prête, on ne merge pas le commit UI qui en dépend.
  6. Si un ticket dépasse ~200 lignes utiles, le scinder (ex. C12/C14 pour scan).

Ordre strict — séquence complète obligatoire

C01 → C08 → [R1] → C09 → C18 → [R2] → C19 → C22 → [R3]
→ C23 → C30 → [R4] → C31 → C34 → C35 → C40 → [R5] → C41 → C43 → [R6]

Aucune étape ne peut être sautée. L'intégration chat (C35-C40) et la qualité (C41-C43) font partie du produit final, pas d'une phase ultérieure optionnelle.

Plan itératif par user stories

Story 0 - Cadrer la faisabilité réelle

Objectif

Verrouiller dans la documentation et dans l'implémentation future que :

  • Luciole-1B reste le composant de structuration sémantique
  • l'OCR brut est assuré par une brique locale dédiée
  • l'OCR image avec Luciole seul n'est pas implémentable réalistement dans ce dépôt

Fichiers de référence

  • README.md
  • android/app/src/main/java/fr/openllm/luciole/cerveau/CerveauServeur.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/SystemPrompt.kt

Story 1 - Définir le modèle ContactCard et le générateur VCF

Objectif

Créer le contrat de données contact indépendant du contrat Action.

Fichiers à créer

  • android/app/src/main/java/fr/openllm/luciole/contact/ContactCard.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/VCardSerializer.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/ContactCardTest.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/VCardSerializerTest.kt

Résultat attendu

  • une structure ContactCard réaliste
  • un export VCARD 3.0 robuste
  • des tests sur champs partiels, caractères spéciaux et listes de téléphones/e-mails

Story 2 - Ajouter un moteur de scan + OCR local derrière des abstractions

Objectif

Lire le texte d'une carte photographiée sur téléphone, en passant d'abord par une étape de scan document qui corrige angle, perspective et orientation.

Décision technique retenue pour le produit

  • OpenCvScanEngine pour la détection des bords, le crop, l'homographie, la rotation et le deskew
  • TesseractOcrEngine basé sur Tesseract4Android
  • CameraX pour la capture
  • aucun composant ne doit dépendre de Google Play Services
  • format de sortie : image corrigée locale, puis OCR local

Fichiers à créer/modifier

  • android/app/build.gradle.kts
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/main/java/fr/openllm/luciole/scan/ScanEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/scan/OpenCvScanEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/TesseractOcrEngine.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrResult.kt
  • android/app/src/main/java/fr/openllm/luciole/ocr/OcrPostProcessor.kt
  • android/app/src/test/java/fr/openllm/luciole/ocr/OcrPostProcessingTest.kt
  • android/app/src/test/java/fr/openllm/luciole/scan/OpenCvScanEngineTest.kt

Séquence d'implémentation

  1. Ajouter les dépendances Gradle pour CameraX, OpenCV et Tesseract4Android.
  2. Créer ScanEngine / OpenCvScanEngine pour :
    • détecter le quadrilatère de la carte
    • permettre un ajustement manuel des coins
    • appliquer la correction de perspective
    • remettre le texte à l'horizontale
  3. Créer OcrEngine / TesseractOcrEngine qui traite l'image corrigée locale.
  4. Créer OcrPostProcessor pour trier les lignes, extraire e-mails/téléphones/URLs.
  5. Gérer les erreurs :
    • contour non détecté → proposer ajustement manuel ou recapture
    • OCR vide → proposer rescan
    • langue/modèles Tesseract absents → message clair et installation locale contrôlée par l'app

Résultat attendu

  • le texte OCR est obtenu localement à partir d'une image déjà corrigée
  • la brique scan et la brique OCR sont encapsulées derrière des interfaces remplaçables
  • le post-traitement produit un OcrResult structuré prêt pour Luciole
  • aucune étape ne dépend d'un service Google ou d'un runtime externe propriétaire

Critères d'acceptation

  • une carte prise en angle produit une image redressée avant OCR
  • le texte OCR contient au minimum les lignes visibles sur la carte
  • les e-mails et numéros sont extraits par regex même si Luciole échoue ensuite
  • le pipeline fonctionne sur un Android générique sans Google Play Services

Story 3 - Réutiliser Luciole-1B pour structurer le texte OCR

Objectif

Transformer le texte OCR en contact structuré.

Fichiers à créer/modifier

  • android/app/src/main/java/fr/openllm/luciole/cerveau/Cerveau.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/CerveauServeur.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/ContactPrompt.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/ContactCardJson.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/ContactCardJsonTest.kt
  • android/app/src/test/java/fr/openllm/luciole/cerveau/CerveauServeurContactTest.kt

Résultat attendu

  • une capacité dédiée du cerveau, par exemple extractContact(rawText: String)
  • un prompt spécifique de structuration contact
  • un parseur JSON séparé de ActionJson

Story 4 - Créer un écran dédié "Scanner une carte"

Objectif

Offrir un parcours utilisateur propre, distinct du chat.

Fichiers à créer/modifier

  • android/app/src/main/java/fr/openllm/luciole/ui/LucioleApp.kt
  • android/app/src/main/java/fr/openllm/luciole/MainActivity.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ScanCarteScreen.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ScanCarteViewModel.kt
  • android/app/src/main/res/values/strings.xml
  • android/app/src/main/res/values-en/strings.xml

Résultat attendu

  • un point d'entrée dédié
  • une séquence capture -> OCR -> structuration -> prévisualisation

Story 5 - Ajouter le déclenchement depuis le chat

Objectif

Permettre à l'utilisateur de lancer le flux dédié depuis Luciole sans mélanger extraction documentaire et routage d'intention.

Fichiers à créer/modifier

  • contract/actions.schema.json
  • android/app/src/main/java/fr/openllm/luciole/model/Action.kt
  • android/app/src/main/java/fr/openllm/luciole/model/ActionJson.kt
  • android/app/src/main/java/fr/openllm/luciole/cerveau/SystemPrompt.kt
  • android/app/src/main/java/fr/openllm/luciole/mains/Mains.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ChatScreen.kt
  • android/app/src/test/java/fr/openllm/luciole/model/ActionScanTest.kt
  • android/app/src/test/java/fr/openllm/luciole/mains/MainsScanTest.kt

Résultat attendu

  • une action du type scanner_carte
  • cette action ouvre le flux spécialisé sans porter elle-même les données contact

Story 6 - Ajouter un brouillon éditable de contact

Objectif

Éviter toute création erronée de contact à cause d'un OCR imparfait.

Fichiers à créer/modifier

  • android/app/src/main/java/fr/openllm/luciole/ui/ContactDraftScreen.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ContactDraftFields.kt
  • android/app/src/main/java/fr/openllm/luciole/ui/ScanCarteViewModel.kt
  • android/app/src/test/java/fr/openllm/luciole/contact/ContactDraftReducerTest.kt

Résultat attendu

  • correction manuelle possible
  • affichage du texte OCR brut pour contrôle
  • validation explicite avant toute sortie

Story 7 - Ajouter l'insertion Android Contacts et l'export VCF

Objectif

Finaliser la fonctionnalité métier.

Fichiers à créer/modifier

  • android/app/src/main/java/fr/openllm/luciole/contact/ContactInsertIntent.kt
  • android/app/src/main/java/fr/openllm/luciole/contact/VcfShare.kt
  • android/app/src/main/java/fr/openllm/luciole/MainActivity.kt
  • android/app/src/main/AndroidManifest.xml
  • android/app/src/test/java/fr/openllm/luciole/contact/ContactInsertIntentTest.kt

Résultat attendu

  • préremplissage d'un contact Android
  • export d'un fichier .vcf partageable

Story 8 - Robustesse, tests et documentation

Objectif

Valider la qualité du flux complet sans dégrader la démo actuelle.

Corpus de test qualité OCR

Constituer un jeu de 15 à 20 cartes réelles ou anonymisées couvrant :

CasObjectif
carte droite, bonne lumièrebaseline
carte inclinée 20-30°valider correction perspective
carte inclinée 45°+limite du système
ombre sur un coinvalider le nettoyage et les filtres locaux
fond chargé (table bois, tissu)valider edge detection
police finestress OCR
carte très graphique / logo dominantstress OCR + structuration
deux numéros + un e-mailvalider extraction multi-champs
carte bilingue FR/ENvalider script latin
carte sans e-mailvalider champs partiels
photo floue légèremesurer dégradation
import galerie (pas capture live)valider parcours alternatif

Métriques à mesurer

MétriqueDescription
scan_success_rate% de cartes où le document est détecté et recadré
ocr_char_accuracycomparaison manuelle texte OCR vs carte
field_precision% de champs corrects (nom, tel, email, société)
field_recall% de champs présents sur la carte qui sont retrouvés
end_to_end_timecapture -> contact prêt à valider
user_correction_rate% de scans nécessitant édition manuelle

Seuils de validation produit

  • scan_success_rate ≥ 85 % sur corpus standard
  • field_precision ≥ 70 % sur téléphone et e-mail
  • end_to_end_time ≤ 12 s sur Pixel récent (hors premier chargement modèles)

Ces seuils sont des critères de finition, pas une excuse pour livrer sans chat, sans VCF ou sans tests d'erreur.

Plan d'escalade si la qualité open source est insuffisante

  1. Mesurer sur le corpus quels cas échouent (angle, ombre, police, fond).
  2. Si échecs principalement géométriques → renforcer OpenCvScanEngine ou intégrer une bibliothèque open source de scan plus mature.
  3. Si échecs principalement OCR → comparer Tesseract4Android et PaddleOCR.
  4. Si échecs principalement sémantiques (nom/poste/société) → améliorer ContactPrompt et heuristiques, pas le scanner.
  5. Conserver les interfaces ScanEngine / OcrEngine pour swap fournisseur sans refonte.

Points à couvrir

  • carte floue
  • plusieurs numéros
  • plusieurs e-mails
  • angle fort
  • ombre
  • OCR vide
  • texte désordonné
  • sortie Luciole partielle
  • appareil modeste avec CPU limité
  • modèles OCR ou données de langue non installés

Fichiers à mettre à jour

  • README.md
  • docs/plans/ocr-carte-visite-vcf.md (résultats benchmark)
  • tests ciblés Android/Kotlin autour des nouveaux parseurs, intents et écrans

Ordre d'implémentation recommandé

Séquence complète, sans report de périmètre :

  1. Story 0 — cadrage
  2. Story 1 — contrat ContactCard + VCF
  3. Story 2 — scan + OCR local
  4. Story 3 — structuration Luciole
  5. Story 4 — écran scan dédié
  6. Story 6 — brouillon éditable
  7. Story 7 — Contacts Android + export VCF
  8. Story 5 — déclenchement depuis le chat
  9. Story 8 — benchmark, robustesse, documentation

Périmètre produit complet

La fonctionnalité n'est pas livrable tant que tous les éléments suivants ne sont pas en place :

  • scan documentaire avec correction géométrique réelle
  • OCR local Tesseract opérationnel
  • structuration ContactCard via Luciole-1B
  • brouillon éditable avec texte OCR visible
  • préremplissage Contacts Android
  • export .vcf partageable
  • action chat scanner_carte
  • corpus de test + benchmark documenté
  • non-régression sur les 11 actions existantes

Parcours utilisateur cible :

je scanne (ou je dis « scanner une carte ») → l'app corrige l'image → OCR local → Luciole structure → je corrige → j'insère le contact ou j'exporte le VCF

Problème qualité OCR et correction géométrique

Pourquoi l'angle fausse l'OCR

Sur une carte de visite photographiée en main, l'échec OCR vient rarement du seul moteur de reconnaissance de caractères. Les causes dominantes sont :

  1. perspective : la carte n'est pas vue de face, les lignes de texte ne sont plus horizontales dans l'image
  2. rotation : la photo est prise portrait/paysage sans que le texte soit remis à l'endroit
  3. recadrage insuffisant : le fond, la main ou la table polluent la zone analysée
  4. qualité visuelle : flou, ombre, reflet, faible contraste, police fine

Conséquence pour Luciole : il ne faut pas envoyer la photo brute directement à l'OCR. Il faut d'abord produire une image « documentifiée » : redressée, recadrée, orientée correctement, puis seulement extraire le texte.

Pipeline qualité cible en deux étages

Étape A — Scan document (géométrie + nettoyage image)
  capture -> détection bords -> correction perspective -> crop -> rotation -> amélioration visuelle

Étape B — OCR texte (reconnaissance caractères)
  image corrigée -> OCR local -> texte brut + blocs/lignes + confiance

Étape C — Structuration sémantique
  texte brut -> Luciole-1B -> ContactCard JSON

Étape D — Validation humaine
  brouillon éditable -> Contacts Android / export VCF

Exigences qualité minimales

ExigenceCible
Carte inclinée jusqu'à ~30-45°lecture exploitable après correction
Photo légèrement flouedégradation acceptable, pas d'échec total
Ombre partielletexte principal toujours lisible
Plusieurs numéros / e-mailstous extraits ou au moins le principal
Carte bilingue FR/ENOCR latin correct
Temps total utilisateurviser < 8-12 s après capture sur appareil récent

Ce que Luciole ne doit pas faire

  • corriger la perspective image
  • détecter les bords du document
  • pivoter l'image
  • faire l'OCR caractère par caractère

Luciole intervient après l'OCR, pour interpréter le texte : distinguer nom, poste, société, téléphone, e-mail, site, adresse.

Recherche technique des solutions mobiles

Cette section synthétise une recherche web ciblée sur les solutions Android capables de gérer angle, recadrage, rotation et deskew avant OCR, en restant compatibles avec une app 100 % locale comme Luciole.

Critères d'évaluation retenus

CritèrePoidsDescription
Correction perspective / deskewélevéindispensable pour cartes prises en main
Rotation / horizontalité autoélevétexte remis à l'endroit avant OCR
OCR on-deviceélevécohérence avec la promesse souveraineté du projet
Intégration Android/Kotlin/Composeélevéfit avec le dépôt actuel
Complexité d'intégrationmoyentemps jusqu'au premier prototype
Coût / licencemoyenacceptable pour une démo vs produit
Dépendance Play Servicesexcluehors cible
Personnalisation UXfaiblerequis pour le produit final

Option 1 - ML Kit (Google, hors cible)

Références

Pile recommandée

// Scan / correction géométrique
implementation("com.google.android.gms:play-services-mlkit-document-scanner:16.0.0")

// OCR texte (script latin, on-device)
implementation("com.google.mlkit:text-recognition:16.0.1")
// ou variante Play Services :
// implementation("com.google.android.gms:play-services-mlkit-text-recognition:19.0.1")

Capacités documentées

FonctionnalitéSupport
Détection automatique du documentoui
Capture automatiqueoui
Edge detectionoui
Correction de perspectiveoui
Rotation automatiqueoui
Crop manuel utilisateuroui
Filtres / nettoyage imageoui (SCANNER_MODE_FULL)
Import galerieoui (configurable)
Traitement on-deviceoui
Permission caméra dans l'appnon requise pour le scanner ML Kit

Contraintes techniques

  • minSdk Document Scanner : API 21+ (le projet est à 31, OK)
  • RAM minimale : 1,7 Go — sinon MlKitException UNSUPPORTED
  • modèles et UI téléchargés via Google Play Services au premier usage
  • impact APK annoncé : ~300 Ko pour le scanner
  • Text Recognition v2 : recommandation Google d'au moins 16x16 px par caractère
  • Text Recognition v2 : retourne blocs, lignes, éléments, score de confiance, orientation

Modes scanner utiles

ModeUsage
SCANNER_MODE_BASEcrop, rotation, réordonnancement
SCANNER_MODE_BASE_WITH_FILTER+ filtres image
SCANNER_MODE_FULL+ nettoyage ML (taches, ombres, doigts) — recommandé

Configuration cible pour Luciole

val options = GmsDocumentScannerOptions.Builder()
    .setGalleryImportAllowed(true)
    .setPageLimit(1)
    .setResultFormats(GmsDocumentScannerOptions.RESULT_FORMAT_JPEG)
    .setScannerMode(GmsDocumentScannerOptions.SCANNER_MODE_FULL)
    .build()

Puis, sur le Uri JPEG retourné :

val image = InputImage.fromFilePath(context, uri)
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
recognizer.process(image)

Forces

  • bon ratio simplicité / qualité pour une intégration Android open source
  • couvre exactement le besoin « angle + crop + rotation » sans réécrire CameraX/OpenCV
  • s'intègre naturellement dans MainActivity via Activity Result API
  • OCR et scan restent locaux

Faiblesses

  • dépendance Google Play Services
  • UI scanner fournie par Google, peu personnalisable
  • qualité OCR perfectible sur polices très stylisées ou cartes très graphiques
  • premier lancement peut nécessiter téléchargement des modèles

Verdict

Solution techniquement intéressante, mais hors cible pour ce plan.

Option 2 - Scanbot SDK (hors cible)

Références

Capacités documentées

FonctionnalitéSupport
Auto-captureoui
Edge detectionoui
Auto-cropoui
Document straighteningoui
Correction perspectiveoui
Contrôle qualité scanoui
OCR offlineoui (OcrEngine)
100 % on-deviceoui

Modèle de licence

  • forfait annuel fixe, sans facturation au scan ni à l'utilisateur
  • prix dépend du nombre d'apps et du pack de fonctionnalités
  • essai gratuit via licence trial

Forces

  • qualité documentaire très élevée
  • excellent sur photos imparfaites, angles, ombres
  • RTU UI mature
  • bon candidat si la qualité scan devient un argument produit fort

Faiblesses

  • SDK commercial, devis obligatoire
  • intégration plus lourde
  • dépendance fournisseur

Verdict

Solution premium documentée, mais hors cible de décision ici.

Option 3 - Dynamsoft Capture Vision (hors cible)

Références

Capacités documentées

FonctionnalitéSupport
Détection bords temps réeloui
Normalisation documentoui
Correction perspectiveoui
Deskewoui
Stabilisation multi-frameoui (MultiFrameResultCrossFilter)
Édition manuelle des coinsoui
Traitement on-deviceoui

Forces

  • contrôle très fin du pipeline de capture
  • robuste sur documents pris en angle
  • bon pour workflows documentaires ambitieux
  • auto-capture stabilisée par comparaison de quads sur plusieurs frames

Faiblesses

  • commercial
  • courbe d'intégration plus technique
  • surdimensionné pour une simple feature carte de visite

Verdict

Très bon choix pro, surtout si l'on prévoit d'autres flux documentaires dans Luciole.

Option 4 - OpenCV + Tesseract (open source)

Références

Capacités

FonctionnalitéSupport
Détection contouroui, mais à coder
Homographie / perspectiveoui, mais à coder
Deskewoui, mais fragile
OCRoui via Tesseract
100 % open sourceoui

Forces

  • aucune dépendance commerciale
  • contrôle total
  • bon pour recherche / prototypage académique

Faiblesses

  • beaucoup de code custom (CameraX + OpenCV + JNI)
  • qualité et maintenance élevées
  • deskew/rotation moins robustes que les SDK documentaires modernes
  • temps d'ingénierie important

Verdict

Non recommandé pour le premier incrément de Luciole, sauf contrainte licence stricte.

Matrice de décision synthétique

SolutionPerspective / deskewRotation autoOCR localIntégrationCoûtFit Luciole
OpenCV + Tesseract4Androidbonouiouiélevéegratuitexcellent
OpenCV + PaddleOCRbonouiouiélevéegratuittrès bon
AndroidDocumentScanner + Tesseract4Androidbonouiouimoyennegratuittrès bon
OpenNoteScanner dérivé + OCRbonouiouimoyennegratuitmoyen

Stratégie qualité OCR pour Luciole

Principe directeur

La qualité finale ne dépend pas d'un seul composant. Elle résulte de la chaîne complète :

bonne capture -> image corrigée -> OCR fiable -> structuration Luciole -> validation humaine

Règles produit à implémenter

  1. Toujours passer par un scan document avant OCR, jamais OCR sur photo brute si évitable.
  2. Limiter à 1 page / 1 carte par scan (pageLimit = 1).
  3. Autoriser l'import galerie, mais appliquer le même pipeline de correction si possible.
  4. Afficher le texte OCR brut à l'utilisateur pour diagnostic.
  5. Ne jamais créer un contact automatiquement sans validation.
  6. Compléter Luciole par des extracteurs déterministes pour téléphone, e-mail, URL — comme le projet le fait déjà pour les numéros dans Extraction.kt.

Post-traitement OCR recommandé (Kotlin)

Après OCR, avant Luciole :

data class OcrBlock(
    val text: String,
    val confidence: Float?,
    val boundingBox: Rect?,
)

data class OcrResult(
    val rawText: String,
    val lines: List<String>,
    val blocks: List<OcrBlock>,
)

Règles de nettoyage :

  • fusionner les lignes dans l'ordre vertical (top -> bottom)
  • supprimer les lignes vides
  • dédupliquer les lignes identiques
  • extraire par regex :
    • e-mails : pattern standard
    • téléphones : réutiliser la logique de Extraction.extractPhone
    • sites web : https?:// ou domaines
  • conserver le texte brut intégral pour Luciole

Complément déterministe + Luciole

Inspiré du pattern existant cerveau route -> mains revalident :

ChampSource primaireSource secondaire
phonesregex / heuristiquesLuciole
emailsregexLuciole
websiteregex URLLuciole
full_nameLucioleheuristique première ligne
companyLucioleheuristique ligne sans @ ni chiffres
job_titleLuciole
addressLuciolelignes multi-mots sans @

Cela réduit la dépendance à la seule intelligence du SLM 1B.

Interfaces techniques cibles

Pour permettre le remplacement de fournisseur sans casser le reste du pipeline :

// android/.../scan/ScanEngine.kt
interface ScanEngine {
    suspend fun scanDocument(): ScanResult
}

data class ScanResult(
    val imageUri: Uri,
    val pageCount: Int = 1,
)

// android/.../ocr/OcrEngine.kt
interface OcrEngine {
    suspend fun recognize(imageUri: Uri): OcrResult
}

// android/.../cerveau/Cerveau.kt — extension
interface Cerveau {
    suspend fun suggest(phrase: String): Action
    suspend fun extractContact(rawText: String): ContactCard
}

Implémentations prévues :

InterfaceImplémentation retenueImplémentation alternative
ScanEngineOpenCvScanEngineAndroidDocumentScannerAdapter, custom
OcrEngineTesseractOcrEnginePaddleOcrEngine, custom
Cerveau.extractContactCerveauServeur + ContactPromptfutur CerveauEmbarqué

Comparatif technique concret des briques OCR mobiles

Le besoin réel ne se limite pas à l'OCR. Il inclut aussi :

  • détection des bords
  • correction de perspective
  • recadrage
  • rotation automatique
  • détection de l'horizontalité
  • deskew avant OCR

Option 1 - ML Kit (hors cible)

Pile recommandée

  • ML Kit Document Scanner
  • ML Kit Text Recognition

Ce que la solution couvre bien

  • détection automatique du document
  • recadrage automatique
  • correction de perspective
  • rotation automatique
  • amélioration visuelle
  • suppression d'ombres et artefacts selon le mode choisi
  • OCR local sur l'image déjà corrigée

Avantages

  • très bon fit Android natif
  • intégration simple
  • faible quantité de code
  • on-device
  • cohérent avec l'architecture actuelle de Luciole

Inconvénients

  • moins de contrôle fin qu'un SDK spécialisé
  • dépendance à Google Play Services
  • qualité parfois un peu moins maîtrisable qu'un SDK premium

Verdict

Solution simple pour Android avec Google, mais hors cible pour ce plan.

Option 2 - Scanbot SDK (hors cible)

Ce que la solution couvre bien

  • auto-capture
  • edge detection
  • auto-crop
  • document straightening
  • correction de perspective
  • OCR offline
  • meilleure maîtrise qualité scan

Avantages

  • excellente qualité documentaire
  • très robuste sur documents photographiés en conditions imparfaites
  • UX scanner mature
  • offline

Inconvénients

  • SDK commercial
  • coût de licence
  • intégration plus lourde

Verdict

Solution premium utile comme benchmark externe, mais hors cible ici.

Option 3 - Dynamsoft (hors cible)

Ce que la solution couvre bien

  • détection temps réel des bords
  • normalisation document
  • correction de perspective
  • deskew
  • stabilisation multi-frame
  • contrôle avancé du pipeline

Avantages

  • très bon niveau technique
  • excellent contrôle de la capture
  • robuste sur scans pris en angle
  • bon pour des workflows documentaires plus ambitieux

Inconvénients

  • commercial
  • intégration plus technique
  • plus lourd qu'une intégration minimale

Verdict

Très bon choix pro si l'objectif va au-delà d'une simple feature OCR carte de visite.

Recommandation finale

La décision d'implémentation est fixée dans Choix techniques retenus.

Résumé en une ligne : CameraX + OpenCvScanEngine + Tesseract4Android + Luciole-1B + validation utilisateur.

Abstractions à introduire dès le départ

  • ScanEngine — correction géométrique interchangeable
  • OcrEngine — OCR interchangeable (Tesseract aujourd'hui, PaddleOCR demain si besoin)
  • ContactCard — contrat métier séparé du JSON d'action

Pipeline stable :

scan/correction -> OCR -> post-traitement -> Luciole -> ContactCard -> Contacts/VCF

Quand changer de brique

SituationAction
Cible générique indépendanteOpenCV + Tesseract4Android (choix actuel)
Qualité OCR insuffisante malgré bonne géométrieremplacer TesseractOcrEngine par PaddleOcrEngine
OpenCvScanEngine trop lent ou fragiles'inspirer de AndroidDocumentScanner (MIT)
Échecs surtout sémantiques (nom/poste)améliorer ContactPrompt, pas le scanner

Risques à surveiller

  • cartes très stylisées
  • polices fines ou fantaisie
  • photos très inclinées (> 45°)
  • ombres fortes et reflets
  • appareils modestes avec CPU limité
  • qualité variable selon les traineddata Tesseract choisis
  • temps global scan -> OCR -> structuration
  • qualité de la séparation nom / poste / société par Luciole-1B
  • confusion chiffres / lettres sur certaines polices (limitation connue OCR généraliste)

Sources et références

RessourceURL
Tesseract4Androidhttps://github.com/adaptech-cz/Tesseract4Android/
Releases Tesseract4Androidhttps://github.com/adaptech-cz/Tesseract4Android/releases
PaddleOCR Android deploymenthttps://www.paddleocr.ai/latest/en/version3.x/inference_deployment/cross_platform/android_deployment.html
PaddleOCR Android repohttps://github.com/PaddlePaddle/PaddleOCR/tree/main/deploy/ppocr-android
paddleocr4androidhttps://github.com/equationl/paddleocr4android
AndroidDocumentScannerhttps://github.com/mayuce/AndroidDocumentScanner
OpenNoteScannerhttps://github.com/allgood/OpenNoteScanner
OSS-DocumentScannerhttps://github.com/ossappscollective/OSS-DocumentScanner/
trudido-scannerhttps://github.com/dominikmuellr/trudido-scanner
OpenCV deskew (référence technique)https://www.dynamsoft.com/codepool/deskew-scanned-document.html
Business card OCR OpenCV+Tesseract (académique)https://stacks.stanford.edu/file/druid:np318ty6250/Sharma_Fujii_Automatic_Contact_Importer.pdf

Recherche technique des solutions mobiles sans Google Play

Nouveau cadrage

Luciole doit fonctionner sur des appareils Android sans Google Play Services.

Conséquence :

  • il n'y a pas de variante Android standard dans ce plan
  • la cible unique repose sur des composants open source ou intégrables sans dépendance Play Services

Dans ce cadrage, il faut séparer très explicitement :

  1. scan documentaire / correction géométrique
  2. OCR
  3. structuration sémantique par Luciole

Critères spécifiques "sans Google Play"

CritèreImportanceCommentaire
Fonctionne sans Play Servicescritiqueexigence principale
Open source ou source intégrablecritiquepour Android générique
Correction perspective / recadragecritiquel'angle fausse l'OCR
Rotation / deskewcritiquehorizontalité du texte
OCR offlinecritiquecohérence avec la promesse locale
Intégration Kotlin/Androidélevéefit avec le dépôt actuel
Maturité / maintenanceélevéeéviter une stack morte
Complexité d'intégrationmoyenneacceptable si gain réel
Licence compatible produitélevéeattention GPL vs MIT/Apache

Option A - OpenCV + Tesseract4Android

Références

Architecture

CameraX -> photo
-> OpenCV (détection quadrilatère, crop, homographie, deskew, rotation)
-> bitmap corrigé
-> Tesseract4Android
-> texte OCR
-> Luciole-1B
-> ContactCard

Ce que couvre la stack

FonctionnalitéSupport
OCR offlineoui
Sans Google Playoui
Open sourceoui
Tesseract moderne Androidoui
Perspective correctionoui, mais à développer avec OpenCV
Deskew / rotationoui, mais à développer avec OpenCV

Points techniques utiles

  • Tesseract4Android est sous Apache 2.0
  • wrapper JNI moderne autour de Tesseract 5.5.1
  • variantes standard et openmp
  • intégration Android actuelle via dépendance :
implementation("cz.adaptech.tesseract4android:tesseract4android:4.9.0")
// ou
implementation("cz.adaptech.tesseract4android:tesseract4android-openmp:4.9.0")

Avantages

  • véritable stack sans Google Play
  • OCR open source mature
  • bon contrôle sur la chaîne complète
  • licence globalement favorable côté OCR

Inconvénients

  • la partie la plus difficile reste à votre charge : détection de la carte, correction de perspective, deskew
  • Tesseract seul n'est pas la meilleure solution sur cartes très graphiques ou polices fines
  • beaucoup plus d'ingénierie qu'une solution managée

Verdict

C'est la base open source la plus réaliste si l'objectif est Android générique sans Google Play.

Option B - PaddleOCR Android

Références

Architecture

CameraX / import image
-> correction géométrique (OpenCV ou composant scanner dédié)
-> PaddleOCR Android SDK
-> texte OCR
-> Luciole-1B
-> ContactCard

Ce que couvre la stack

FonctionnalitéSupport
OCR offlineoui
Sans Google Playoui
Open sourceoui
Détection + reconnaissance end-to-endoui
Perspective correctionpartiellement, souvent à compléter
Deskew / scan docpas la brique principale

Points techniques utiles

  • démo Android officielle PP-OCRv6
  • architecture SDK + Demo
  • déploiement Android basé sur ONNX Runtime
  • minSdk 26
  • dépendances supplémentaires typiques :
implementation(files("libs/ppocr-sdk-release.aar"))
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.21.1")
implementation("com.quickbirdstudios:opencv:4.5.3")

Avantages

  • OCR open source plus moderne que Tesseract dans beaucoup de cas
  • pipeline OCR mobile plus ambitieux
  • pas de dépendance Play Services

Inconvénients

  • intégration plus lourde
  • tailles de modèles et dépendances plus conséquentes
  • pour la carte de visite, il faut souvent tout de même une vraie brique de scan géométrique en amont
  • minSdk 26, alors que le projet actuel supporte 31, donc compatible ici, mais moins universel

Verdict

Très intéressant si la qualité OCR pure devient prioritaire, mais pas suffisant seul pour régler le problème d'angle. Il faut souvent l'associer à OpenCV.

Option C - Bibliothèques / projets open source de scan documentaire Android

Références

Ces projets sont utiles non seulement comme bibliothèque potentielle, mais aussi comme référence d'implémentation pour la détection des bords, le crop manuel et la correction de perspective.

C1 - OpenNoteScanner

Caractéristiques

  • projet Android open source historique
  • scan documentaire avec OpenCV
  • détection de bords
  • correction de perspective
  • export image/PDF
  • disponible aussi via F-Droid

Attention

  • historique de dépendance à OpenCV Manager
  • licence GPLv3

Verdict

Très utile comme référence technique, mais moins idéale comme dépendance directe dans une app si la contrainte GPL est problématique.

C2 - AndroidDocumentScanner

Caractéristiques

  • bibliothèque Android basée sur OpenCV
  • licence MIT
  • objectif explicite : scanner des documents façon CamScanner

Verdict

Candidat plus simple que OpenNoteScanner pour réutiliser une brique de scan open source avec licence plus souple.

C3 - trudido-scanner

Caractéristiques

  • bibliothèque Android récente
  • CameraX + OpenCV + C++17/JNI
  • sans Play Services
  • détection de coins de document
  • crop interactif
  • module AAR réutilisable

Attention

  • projet très récent
  • peu de recul / peu d'adoption
  • licence GPLv3

Verdict

Intéressant comme inspiration moderne, mais maturité encore faible.

C4 - OSS-DocumentScanner

Caractéristiques

  • projet actif
  • licence MIT
  • utilise OpenCV et Tesseract
  • orienté application plus que petite bibliothèque Android native pure

Verdict

Très utile comme base d'étude pour le pipeline document + OCR offline, mais probablement trop gros pour être intégré tel quel.

Matrice comparative "sans Google Play"

SolutionSans PlayOpen sourceScan géométriqueOCRLicenceComplexitéRecommandation
OpenCV + Tesseract4Androidouiouià coderouiApache 2.0 + dépendancesélevéebase recommandée
OpenCV + PaddleOCRouiouià coderouiopen sourceélevéetrès bon si priorité OCR
AndroidDocumentScanner + Tesseract4AndroidouiouiouiouiMIT + Apachemoyennetrès bon compromis
OpenNoteScanner + TesseractouiouiouipossibleGPLv3moyennebon pour étude, moins bon pour intégration produit
trudido-scanner + Tesseract/PaddleOCRouiouiouià ajouterGPLv3moyenne/élevéeprometteur mais immature

Décision révisée pour Android générique

Si la contrainte devient :

« fonctionner sans Google Play, sur Android générique, avec des composants open source »

alors la recommandation change.

Nouvelle recommandation cible

Recommandation 1 - compromis le plus réaliste

CameraX
-> ScanEngine basé sur OpenCV / bibliothèque open source de scan
-> OcrEngine basé sur Tesseract4Android
-> OcrPostProcessor
-> Luciole-1B
-> ContactCard

Option concrète à étudier en priorité :

  • AndroidDocumentScanner ou implémentation OpenCV maison pour le scan
  • Tesseract4Android pour l'OCR

Recommandation 2 - si la qualité OCR brute est prioritaire

CameraX
-> OpenCV / scan doc
-> PaddleOCR Android
-> OcrPostProcessor
-> Luciole-1B
-> ContactCard

Pourquoi cette révision

  • les solutions dépendantes de Google Play ne correspondent pas à la cible
  • le problème principal reste la géométrie de l'image
  • il faut une stack qui garde la maîtrise sur :
    • perspective
    • rotation
    • crop
    • deskew
    • OCR offline

Impact sur le plan d'implémentation

Story 2 révisée pour la cible unique Android générique

La story 2 doit prévoir une seule pile fournisseur :

  • OpenCvScanEngine
  • TesseractOcrEngine

avec possibilité ultérieure de remplacer uniquement TesseractOcrEngine par PaddleOcrEngine si les mesures le justifient.

Interfaces à figer impérativement

interface ScanEngine {
    suspend fun scanDocument(): ScanResult
}

interface OcrEngine {
    suspend fun recognize(imageUri: Uri): OcrResult
}

C'est ce découplage qui permettra de garder le reste du plan inchangé :

scan -> OCR -> Luciole -> ContactCard -> Contacts/VCF

Conclusion

Le plan réaliste pour Luciole est :

  1. scanner et corriger l'image localement (CameraX + OpenCvScanEngine)
  2. effectuer l'OCR localement sur l'image corrigée (Tesseract4Android)
  3. post-traiter le texte (regex + heuristiques, sur le modèle de Extraction.kt)
  4. utiliser Luciole-1B pour structurer intelligemment le texte en ContactCard
  5. faire valider le résultat par l'utilisateur avant insertion contact ou export VCF

Ce choix respecte à la fois l'architecture actuelle du dépôt, les limites réelles du modèle, le besoin de qualité OCR sur photos en angle, et l'objectif produit demandé.

La qualité ne viendra pas d'un seul composant : elle dépendra surtout de la correction géométrique avant OCR, puis de la validation humaine et du complément déterministe autour de Luciole.