# Post-traitement et moteur de règles (`app/postprocess.py`, `app/rules.py`) ## `postprocess.py` — du texte brut aux valeurs métier ### `_norm_number(s)` Normalise un nombre lu par le CRNN en `float` : retire les espaces, gère la virgule décimale française ("14,28" → 14.28) et les séparateurs de milliers quand virgule ET point sont présents ("17.500,00" → 17500.00). ### `process(recognized_fields, fuel_price=None, cfg=None)` Point d'entrée principal. Contrairement à l'ancien pipeline OCR générique (`src/postprocess.py` à la racine du dépôt), **pas d'heuristique de classification** ici : le CRNN a déjà étiqueté chaque ligne par position (`field: "prix"|"volume"|"prix_litre"`), donc on normalise et calcule directement. **Règle la plus importante du fichier** : ```python price_val = fuel_price if fuel_price is not None else screen_price_val ``` Le prix du litre **configuré côté YELY** (paramètre `fuel_price` de l'appelant) fait toujours autorité sur celui lu à l'écran. C'est la règle métier n°1 du cahier des charges ("le prix du litre doit être configurable depuis le système YELY"), et ça corrige un bug réel rencontré en session : sur certaines pompes, le libellé "Prix" désigne en fait le **montant total**, pas le prix unitaire — un ancien pipeline qui faisait confiance à la valeur lue à l'écran pour le prix unitaire pouvait donc confondre montant et prix, et bloquer un paiement pourtant cohérent. ### `evaluate_consistency(liters, amount, price, tol, cfg)` Implémente la règle centrale du §5.5 : `montant = litres × prix`, avec une tolérance (`cfg.consistency_tolerance`, 2% par défaut — pour absorber les arrondis d'affichage pompe). Calcule aussi la valeur manquante quand une seule des deux (litres ou montant) est lisible (§5.4/§6.4/§6.5). ## `rules.py` — décider succès ou blocage `evaluate(parsed, quality, fuel_price, cfg)` applique les vérifications du §5.6/§10 **dans un ordre précis, la première qui échoue l'emporte** : 1. Qualité image non "valid" → bloqué (photo floue/sombre/surexposée) 2. Aucune donnée numérique détectée → bloqué 3. Litres ET montant manquants, ou prix manquant → bloqué 4. Incohérence détectée (`is_consistent` pas `True`) → bloqué 5. Score de confiance sous le seuil (`cfg.business_confidence_threshold`) → bloqué 6. Sinon → succès Cet ordre est délibéré : par exemple, une image floue doit toujours produire le message "photo floue" même si, par coïncidence, des chiffres ont quand même été lus — le pompiste doit reprendre la photo, pas être induit en erreur par un résultat qui a l'air valide. ### Score de confiance ```python confidence_score = ocr_confidence * 0.6 + quality_score * 0.4 ``` Combine la confiance moyenne du CRNN sur les champs lus et le score de qualité image. Simple et explicable (utile pour justifier une décision de blocage au pompiste), mais **pas appris** — une piste d'amélioration futur serait d'entraîner un petit modèle de calibration sur des données réelles de succès/échec, plutôt qu'une pondération fixe choisie à la main. ## Pourquoi ces deux fichiers sont séparés `postprocess.py` ne fait aucune I/O et ne dépend d'aucun modèle : il se teste en quelques millisecondes (`tests/test_postprocess.py`, ~11 tests, aucun chargement du CRNN). `rules.py` encapsule uniquement la **décision** (les seuils métier) — on peut ajuster `config.yaml` sans toucher au calcul, et inversement changer une formule de calcul sans re-tester la logique de blocage.