Actualités

Bon Dossier : trois versions, un même système de reconnaissance des tickets sur l’appareil

Comment un scanner de tickets est devenu, à partir d’un paquet Swift testable, une plateforme iPhone, iPad et macOS respectueuse de la vie privée : OCR, Core ML, synchronisation CloudKit, exports, outils d’entraînement, audits de production et une compréhension durement acquise des difficultés que les tickets posent au logiciel.

Bon Dossier : trois versions, un même système de reconnaissance des tickets sur l’appareil

La difficulté de Bon Dossier ne venait pas d’une interface compliquée, mais du monde réel, peu compatible avec des données propres. Les tickets sont étroits, courbés, effacés, pliés, photographiés sous une mauvaise lumière et imprimés dans plusieurs langues. Ils mêlent récapitulatifs de taxes, textes de fidélité, métadonnées de terminaux, réductions, remboursements, numéros ressemblant à des codes-barres et totaux parfois répartis sur plusieurs lignes. Une démonstration d’OCR peut reconnaître le texte. Une application en production doit déterminer lequel fait foi.

Le projet a donc traversé trois versions concrètes. Ce sont moins des éditions commerciales que des étapes d’ingénierie : les fondations, le moteur de reconnaissance, puis la plateforme prête pour la production. Chacune a résolu une catégorie de problèmes tout en révélant la suivante.

Version 1

Les fondations

Un paquet Swift, un modèle métier, un analyseur, une couche d’export et une interface SwiftUI ont permis de tester les tickets avant même l’appareil photo et la synchronisation.

Version 2

Le moteur de reconnaissance intelligente

OCR Vision, classificateurs Core ML, zones de mise en page de type YOLO, assemblage des lignes d’articles, critères de validation et apprentissage local ont remplacé l’analyse naïve.

Version 3

La plateforme de production

Core Data, CloudKit, sauvegardes, extensions de partage, exports, métadonnées d’archive, macOS, audits et applications d’entraînement ont transformé le moteur de reconnaissance en produit.

La contrainte initiale : confidentialité, traitement local et vérifiabilité

La décision d’architecture la plus importante a précédé le scanner : ReceiptVault devait être utile sans envoyer des documents financiers personnels à un serveur. Il fallait donc renoncer à la solution facile consistant à transmettre les images à un service d’OCR ou de modèle de langage en ligne. L’application de production reposerait sur Apple Vision, VisionKit, Core ML, des analyseurs Swift et une validation déterministe, le tout en local. iCloud pouvait synchroniser l’archive privée de l’utilisateur, mais il n’y aurait pas de backend Bon Dossier.

Cette exigence de confidentialité a structuré tous les processus suivants. Les données d’entraînement restaient des ressources locales du projet. Les corrections étaient conservées comme exemples locaux. Les modèles candidats n’étaient retenus qu’après rapports et contrôles de non-régression. Même la couche Apple Foundation Models était prévue comme une vérification locale facultative, avec validation stricte du schéma JSON et repli sur le modèle de tickets fourni dans l’application.

Version 1 : construire le socle avant l’appareil photo

La première version était volontairement discrète. Au lieu de commencer par un écran de prise de vue en espérant que le reste suivrait, nous avons créé un paquet Swift testable nommé ReceiptVaultCore. Il contenait le modèle métier, l’analyseur de tickets, la catégorisation, la détection des doublons, l’export et, plus tard, l’essentiel de la reconnaissance et de la persistance. Le premier objectif était simple : rendre le modèle de tickets exécutable et testable sans dépendre du simulateur, des autorisations de caméra ou d’un OCR en direct.

Le modèle central s’est construit autour de ParsedReceipt : commerçant, adresse, date d’achat, devise, total, sous-total, taxe, moyen de paiement, numéro du ticket, texte OCR, notes, lignes d’articles, fichiers numérisés et état de vérification. S’y sont ajoutés les exports TSV et Excel, le regroupement des doublons, la déduction des catégories, les rappels d’échéance et la validation de qualité. L’interface pouvait désormais traiter les tickets comme de véritables enregistrements plutôt que des chaînes OCR dispersées.

L’interface SwiftUI suivait le même principe. Sur iPhone, elle proposait les onglets Archives, Numériser, Analyses et Réglages. Sur iPad, une barre latérale et une navigation à plusieurs volets. Les vues utilisaient les conteneurs système TabView, NavigationStack, NavigationSplitView, List et Section plutôt qu’un framework d’interface personnalisé. L’application restait ainsi native, accessible et mesurable.

Première grande décision de méthode : construire le produit par petites couches vérifiables. Les tests métier précédaient l’intégration de la caméra. Le comportement de l’analyseur était fixé par des jeux de test. Le projet Xcode était généré et protégé par des scripts. Les commandes de compilation et de test étaient consignées pour rendre le projet reproductible, au lieu de le maintenir manuellement à bout de bras dans Xcode.

Outils de la version 1

  • Swift 6 et SwiftPM pour le module central partagé.
  • Génération du projet Xcode par des scripts Ruby, avec protections de la synchronisation des sources.
  • XCTest pour les contrôles de non-régression de l’analyseur, des exports, des archives et du code source.
  • Scripts de compilation sur simulateur pour vérifier iPhone et iPad.
  • Images de tickets de référence et reconstruction du texte pour mesurer l’effet des changements de l’analyseur.
  • Document de suivi vivant indiquant les travaux terminés, en cours et bloqués.

Un premier obstacle d’infrastructure était le décalage des outils : la version de Swift en ligne de commande ne correspondait pas toujours au SDK Xcode. La solution était explicite et reproductible : utiliser xcrun swift test et la chaîne d’outils sélectionnée par Xcode pour les commandes du paquet. Ce détail banal a évité de confondre des erreurs d’environnement avec des régressions de l’application.

Version 2 : du texte OCR à la compréhension des tickets

La deuxième version a commencé lorsque l’OCR seul s’est révélé insuffisant. Apple Vision peut lire de nombreuses lignes, sans savoir quel nombre est le total final, quelle ligne est une base de TVA, quel montant est une réduction, quelle date provient du terminal ou où commence le bloc d’articles. Bon Dossier avait besoin d’un moteur de compréhension des tickets.

La numérisation utilisait VisionKit sur iPhone et iPad, en conservant les originaux tandis que l’OCR et l’analyse pouvaient travailler sur des versions recadrées ou redressées. La sortie OCR devenait une source d’information, plus une vérité établie. Les lignes reconnues, leurs cadres, scores de confiance et, plus tard, les structures documentaires entraient dans une chaîne hybride : les modèles Core ML locaux proposaient des rôles et des zones, les analyseurs Swift assemblaient des résultats candidats et des validateurs déterministes décidaient lesquels accepter.

Le projet est ainsi passé d’une application avec des règles d’analyse à un système d’apprentissage automatique hors ligne. Des scripts ont été ajoutés pour amorcer les annotations, entraîner les classificateurs de lignes, détecteurs et localisateurs, préparer les jeux YOLO, évaluer des mises en page inédites, générer des exemples négatifs autres que des tickets, auditer les annotations, dériver des jeux spécialisés et vérifier les entraînements. L’application restait locale et déterministe, tandis que le développement suivait une boucle : mesurer, isoler, annoter, entraîner, comparer et sélectionner.

La chaîne de reconnaissance

CoucheObjectifOutils représentatifs
CaptureAcquérir les images des tickets en conservant les originaux pour les vérifications, partages et sauvegardes ultérieurs.VisionKit, AVFoundation, vérification des numérisations avec SwiftUI, recadrage et rotation.
Extraction du texteReconnaître les lignes OCR, leur géométrie, leur confiance et les structures du document.OCR Apple Vision, liste fixe de langues de tickets à reconnaître, structure documentaire d’iOS 26 lorsqu’elle est disponible.
Mise en pageRepérer le papier du ticket et les zones du commerçant, des articles, du total, de la TVA et du paiement.Entraînement YOLO/Ultralytics, exports des localisateurs Core ML, audits de structure et évaluation des zones.
SémantiqueClasser les lignes et montants : commerçant, article, total, TVA, paiement, métadonnées ou éléments à ignorer.Classificateurs Create ML/Core ML, modèle de repli JSON, classeurs de champs et spécialistes de zones.
AssemblageConstruire les lignes d’articles, totaux, ventilations de taxes et informations de paiement à partir de lignes candidates imparfaites.Analyseurs Swift, assembleur de lignes d’articles, analyseur guidé par les zones et arbitre des lignes.
ValidationAccepter uniquement les résultats qui respectent les calculs du ticket et évitent les confusions avec les terminaux, codes-barres et taxes.Validateur de qualité, détection des devises, calculs TVA comprise, validation des totaux finaux et jeux de non-régression.

Le changement technique décisif : la validation prend le contrôle

Un modèle peut proposer et un analyseur déduire, mais les comptes du ticket doivent tomber juste. Bon Dossier a donc fait de la validation arithmétique son arbitre : la somme des articles doit correspondre au total final, la TVA ne doit pas être comptée deux fois, les sous-totaux et bases de taxes ne doivent pas remplacer le total final, et les métadonnées de paiement ou de carte ne doivent pas devenir des montants d’achat.

Cette règle a révélé des bugs subtils. Pour les tickets européens TVA comprise, le bon contrôle est généralement total == sum(items), et non total == items + tax. Le second compte la TVA deux fois et peut valider précisément le mauvais total. La correction de cette règle a fait passer une large catégorie de tickets d’un résultat « plausible » à un résultat mathématiquement juste.

D’autres corrections étaient tout aussi concrètes : les récapitulatifs de réductions ne remplaçaient plus le total final ; les en-têtes « Prix » et « Total » ne confondaient plus prix d’article et total du ticket ; les bases de taxes ne primaient plus sur le montant payé ; les libellés séparés du montant, comme « Total » suivi de sa valeur sur la ligne suivante, recevaient un traitement spécifique. Ces changements corrigeaient la représentation du réel par l’application, au-delà de simples ajustements cosmétiques.

Des données d’entraînement sans se tromper soi-même

Le projet utilisait plusieurs niveaux d’annotations, car toutes les données d’entraînement n’offrent pas la même confiance. Les annotations validées à la main formaient le niveau de référence « gold ». Les exemples issus de Vision ou des sauvegardes de l’application étaient classés en données d’amorçage ou « silver ». Les corrections manuelles étaient conservées avec leur traçabilité. La chaîne de production évitait expressément d’écraser les annotations YOLO acceptées, et les modèles candidats devaient être évalués sur des ensembles fixes de validation et de test.

Cette rigueur compte, car les systèmes de tickets sont faciles à surajuster involontairement. Un bug documenté rendait l’entraînement des spécialistes de zones sans valeur : l’entrée contenait une caractéristique révélant presque la réponse, donnant au modèle un avantage artificiel. La correction a supprimé cette fuite des données d’entraînement et d’exécution, obligeant les spécialistes à se fonder sur de vrais indices. Un autre nettoyage a supprimé les raccourcis codés en dur pour certains commerçants et les alias de fautes OCR. Le résultat était plus honnête et temporairement plus difficile, mais les problèmes restants devenaient visibles au lieu d’être masqués par des artifices.

Quelques mesures de qualité enregistrées pendant le projet

  • Un benchmark récent sur appareil portait sur 728 tickets et 35 361 lignes OCR.
  • Les résultats attendus vérifiés étaient atteints pour 545 tickets sur 546, avec une divergence de date encore visible.
  • La précision de classification des lignes atteignait 96,3 %.
  • Le temps moyen d’analyse était d’environ 47,5 ms, avec un 95e percentile d’environ 81,4 ms sur la chaîne locale sans réseau.
  • L’ambition de publication restait supérieure : approcher 99 % d’automatisation nécessite davantage de vrais tickets vérifiés et de lignes d’articles annotées.

Version 3 : transformer le moteur en produit

La troisième version concernait tout ce qui rend un moteur utile : persistance, synchronisation, recherche, import de fichiers, sauvegardes, exports, réglages, manifestes de confidentialité, notes App Store, accessibilité, performance, traduction, macOS et outils d’exploitation. C’est souvent là que les prototypes échouent, car l’ingénierie produit couvre surtout le travail invisible jusqu’à ce qu’il dysfonctionne.

La persistance avait commencé par une archive JSON locale, simple à consulter et rapide à faire évoluer. Cela convenait au début, mais pas à une archive privée iPhone/iPad de production. Le projet a évolué vers une abstraction de dépôt avec Core Data comme stockage principal et NSPersistentCloudKitContainer pour la synchronisation iCloud privée. La conception coordonnait explicitement originaux numérisés, sommes de contrôle, vignettes et métadonnées, sans réduire le ticket à une simple ligne de table.

L’archive est aussi devenue plus qu’une liste. Elle a adopté une organisation proche de Mail : Tous les tickets, dossiers, dossiers intelligents, indicateurs, recherche ciblée, sélection, déplacement, export et transmission par e-mail. Les imports se sont ouverts à Fichiers, Mail, images, PDF et textes. App Intents et liens profonds pouvaient orienter vers Archives, Numériser, Analyses et Réglages. Spotlight et l’extension de partage ont intégré les tickets au système au lieu de les enfermer dans l’application.

Core Data et CloudKit : les difficultés étaient opérationnelles

Le choix de Core Data était pragmatique. SwiftData convient bien aux prototypes, mais ReceiptVault exigeait un contrôle explicite des migrations, la gestion des fichiers binaires, des points de diagnostic CloudKit, le traitement des conflits et des années d’évolution du schéma. Core Data offrait ces moyens. L’interface pouvait conserver un dépôt simple, tandis que la couche de stockage gérait la migration depuis JSON et la restauration des fichiers de numérisation manquants.

Les risques opérationnels étaient réels. L’initialisation du schéma CloudKit devait inclure les entités définies par programme : tickets, fichiers numérisés, dossiers, marqueurs et dossiers intelligents. Les schémas vides entraînaient des échecs partiels. Les versions publiées exigeaient le déploiement du schéma dans l’environnement de production, et les autorisations devaient utiliser APNs en production pour les notifications silencieuses de synchronisation. Ce sont des problèmes discrets, mais ils déterminent si la synchronisation fonctionne réellement.

Sauvegardes, exports et coût des originaux

Bon Dossier conserve des justificatifs financiers : l’export et la sauvegarde ne pouvaient pas être traités après coup. L’application a ajouté l’export TSV, un véritable export XLSX, l’export d’archives filtrées ou de tickets sélectionnés, les sauvegardes et restaurations manuelles et les feuilles de partage. Elle conserve aussi les pages originales, plutôt que jeter les preuves après l’analyse. C’est le bon choix produit, mais il impose un travail de performance et de stockage.

Un audit de production a clairement identifié le compromis : conserver les images sur disque et dans des enregistrements binaires externes Core Data double le stockage local, tandis que des sauvegardes JSON intégrant tous les fichiers peuvent provoquer des pics de mémoire. La voie de résolution était claire : déplacer les opérations coûteuses de dépôt et de sauvegarde hors du thread principal, traiter les grandes sauvegardes en flux ou par blocs, mettre les vignettes en cache et choisir explicitement entre Core Data et le système de fichiers comme source unique des octets numérisés.

La version macOS dépasse une simple visionneuse

L’ambition macOS était volontairement élevée : un ReceiptVault de bureau natif, partageant l’archive iCloud privée et la logique centrale, au-delà d’un compagnon en lecture seule. L’interface comprend trois volets : barre latérale, liste des tickets et détail. La cible Mac réutilise le cœur partagé, la chaîne OCR/ML et la logique d’archive, d’export, de sauvegarde et de synchronisation, en adaptant présentation, commandes, import et capture aux conventions macOS.

Cela a ajouté des besoins : autorisations macOS, bac à sable, accès aux fichiers choisis, textes d’autorisation de caméra, commandes de menus, préparation de la caméra Continuité, fenêtres d’import natives et publication multiplateforme sur l’App Store. Cela a aussi confirmé l’intérêt de séparer ReceiptVaultCore des interfaces. La présentation peut varier selon la plateforme ; la logique de reconnaissance doit rester commune.

Les méthodes d’ingénierie derrière le projet

Le projet a tenu parce qu’il n’a pas été mené comme une immense accumulation de fonctionnalités. Il s’est appuyé sur des livraisons par couches, audits de code, scripts locaux, jeux de test, rapports de modèles et critères de validation documentés. Parmi ses ressources les plus importantes figuraient les scripts et rapports qui rendaient le travail mesurable, au-delà des écrans visibles.

Compilation et vérification

Tests SwiftPM, compilations sur simulateur, tests de protection des sources, vérification du générateur de projet, contrôles de traduction, scripts d’intégration continue et vérification manuelle des captures ont évité les dérives.

Données et apprentissage automatique

La chaîne séparait les annotations gold, silver et d’amorçage, conservait les fichiers annexes, auditait les structures YOLO, entraînait les modèles locaux et ne retenait les résultats qu’après comparaison.

Renforcement du produit

Audits de performance, inventaire de confidentialité, notes App Store, contrôles d’accessibilité, diagnostics de synchronisation et revues des obstacles à la publication ont rendu visibles les défaillances cachées.

Principaux outils et techniques

  • Swift et SwiftUI : interfaces iPhone, iPad et macOS avec listes, vues à plusieurs volets, réglages et parcours de partage natifs.
  • SwiftPM : module central partagé et exécutable d’outils ML pour tester la logique métier en dehors de la cible de l’application.
  • XCTest : tests de l’analyseur, de l’archive, des sauvegardes, exports, traductions, protections de sources, App Intents et contrats ML.
  • Apple Vision et VisionKit : capture des numérisations, OCR, géométrie des lignes et extraction de la structure documentaire.
  • Core ML et Create ML : détecteur de tickets, localisateur, classificateur, classement des champs et modèles locaux de rôle des lignes.
  • YOLO et Ultralytics : expérimentation des zones de mise en page, annotations et exports de modèles pour le papier du ticket et les régions de contenu.
  • Scripts Python, Swift et Ruby : préparation des données, génération du projet, entraînement des modèles, audits, rapports et synchronisation des ressources.
  • Core Data et CloudKit : stockage privé synchronisé de l’archive, migration depuis JSON et réplication des fichiers numérisés.
  • CryptoKit : sommes de contrôle SHA-256 pour l’intégrité des numérisations et préparation de futures sauvegardes chiffrées.
  • Intégration à l’écosystème : extension de partage, import documentaire, App Intents, liens profonds, entrées Spotlight, export de type Mail et sauvegardes manuelles.

Les obstacles et leurs solutions

ObstacleEnjeuPiste de solution
L’OCR produisait du texte, pas une vérité établie.Totaux, bases de TVA, lignes de paiement et numéros de codes-barres se ressemblaient sous forme de chaînes brutes.Combiner géométrie OCR, rôles Core ML, contexte des zones et validation déterministe.
Les lignes d’articles étaient mal structurées.Descriptions, quantités et prix apparaissaient souvent dans des colonnes ou blocs visuels distincts.Créer un assembleur de lignes, utiliser les zones d’articles et ajouter des arbitres avec contrôle des sommes.
Les modèles pouvaient se surajuster sans signe visible.Les fuites d’annotations et raccourcis codés pour certains commerçants amélioraient artificiellement les scores.Supprimer les fuites de réponses, séparer les niveaux de confiance des annotations et remplacer les artifices par la mémoire des corrections et des filtres génériques.
La synchronisation CloudKit exigeait une configuration précise.Un schéma de production manquant ou une mauvaise autorisation APNs pouvait faire échouer la synchronisation malgré un code correct.Ajouter l’initialisation explicite du schéma, les audits de publication et les contrôles TestFlight sur plusieurs appareils.
Les numérisations originales pesaient sur la mémoire et le stockage.Les grands tickets, sauvegardes et images stockées en double pouvaient bloquer l’interface ou provoquer des pics de mémoire.Mettre les vignettes en cache, déplacer le travail du dépôt hors du thread principal, sauvegarder en flux et rationaliser le stockage des numérisations.
Les exigences de publication étaient faciles à sous-estimer.Manifestes de confidentialité, autorisations, protection des données et notes App Store pouvaient bloquer l’envoi ou l’examen de l’application.Auditer la préparation à la production avant la dernière phase de développement, et non après la compilation du binaire.

Pourquoi cet effort d’ingénierie était considérable

La difficulté ne tenait pas à un algorithme, mais au nombre d’interfaces qui devaient toutes fonctionner ensemble. Une numérisation devait devenir un résultat OCR, puis un ticket recherchable, exportable, synchronisable, restaurable et compréhensible. La reconnaissance devait s’améliorer sans fuite de données ni serveur. L’interface devait rester native et calme, tandis que le traitement sous-jacent relevait davantage de l’expertise documentaire que de la simple lecture de formulaires.

Chaque version faisait paraître la précédente plus petite. La version 1 prouvait que l’application pouvait exister comme produit Swift bien structuré. La version 2 montrait que comprendre les tickets exigeait une vraie chaîne locale de ML et de validation. La version 3 confirmait qu’un moteur de reconnaissance ne devient un produit que lorsque persistance, synchronisation, import, export, confidentialité, performance et publication sont fiables.

Le code porte désormais la trace de ce parcours : cœur partagé, interface applicative, services de numérisation, vues d’archives, dépôts Core Data, configuration CloudKit, contrats ML, caches de modèles, scripts d’entraînement, outils de production, documentation App Store, audits de performance et dizaines de tests ciblés. Le résultat dépasse un simple habillage d’OCR : c’est une plateforme locale de reconnaissance intelligente des tickets qui a dû apprendre à distinguer texte et justificatif fiable.

Leçons pour des projets similaires

  1. Commencez par un cœur testable. La caméra et l’OCR produisent du bruit ; un modèle métier et des tests de référence donnent des fondations au projet.
  2. Traitez l’OCR comme un indice, pas comme le résultat final. Géométrie, confiance, mise en page et arithmétique du ticket participent à l’exactitude.
  3. Séparez la proposition du modèle de son acceptation par le produit. Une validation déterministe doit décider si l’analyse est suffisante.
  4. Évitez les scores d’entraînement trompeurs. Protégez-vous contre les fuites d’information, raccourcis codés en dur et contaminations de l’ensemble de validation.
  5. Anticipez les fichiers dès le départ. Les images originales influencent le stockage, les sauvegardes, la synchronisation et les performances.
  6. Auditez la configuration de production comme le code. Autorisations, manifestes de confidentialité et schémas CloudKit font partie de l’application.
  7. Gardez une interface spécifique à chaque plateforme aussi légère que possible. Le cœur partagé a permis iPhone, iPad et macOS sans dupliquer le moteur de reconnaissance.

L’histoire de Bon Dossier rappelle qu’une intelligence locale sérieuse dépasse une fonction isolée. C’est une chaîne de fiabilité : capture, OCR, mise en page, classification, analyse, validation, persistance, synchronisation, export et rigueur de publication. Si un maillon cède, l’utilisateur juge l’ensemble incorrect. En renforçant chaque étape, un ticket papier désordonné devient une donnée privée fiable.

Retour aux actualités

À découvrir

Articles associés

22 mai 2026

Hamlet : développer un éditeur de publication professionnel pour 4D

Hamlet est l’un des développements majeurs de notre feuille de route : un éditeur natif de texte enrichi, de mise en page et de publication depuis les données, conçu pour 4D. L’objectif : offrir un éditeur complet intégrable dans un formulaire 4D, utilisant les données réelles de la base pour produire des factures, rapports, lettres, modèles et publications professionnels.

7 mai 2026

4D Agenda : du code de l’ère Carbon en 2003 à un plug-in macOS moderne en 2026

Nous avons achevé une modernisation majeure de 4D Agenda : ce plug-in classique du début des années 2000 dispose désormais d’un code macOS propre, stable et moderne. Le code d’origine, datant d’environ 2003, avait longtemps bien servi, mais accumulait les anciens rendus, branches de plateformes, hypothèses du Mac classique et couches de compatibilité dignes d’un musée.

2 mai 2026

Dix heures de débogage dans AccountView

Parfois, la maintenance logicielle est claire et prévisible. Et parfois, ce sont dix heures face à Visual FoxPro, des contrôles ActiveX anciens, des fichiers DBF temporaires, du code compilé brouillé, des visionneuses cachées, des composants PDF qui plantent et un système de gestion du transport obstiné qui refuse de livrer ses secrets.

Vous souhaitez travailler avec nous ?

Contactez-nous pour échanger sur votre projet.