# Monitoring et apprentissage continu (`monitoring/`) Deux besoins distincts couverts ici : **savoir comment le modèle se comporte en production** (suivi des performances), et **transformer les erreurs observées en nouvelles données d'entraînement** (apprentissage continu). Aucun des deux ne modifie le comportement de `/analyze` — ce sont des composants qui lisent/complètent les traces déjà produites par l'API. ## 1. Suivi des performances Chaque appel à `POST /analyze` écrit déjà une ligne JSON dans `logs/api.log` (requête sans l'image + réponse complète + `model_version`). `monitoring/metrics.py` parse ce fichier et calcule, sans état en mémoire ni base de données : - `total_requests` : nombre d'appels journalisés. - `success_rate` : proportion de réponses `success=true`. - `avg_confidence_score` : moyenne des `confidence_score` renvoyés. - `blocking_causes` : histogramme des `message` pour les réponses bloquées (utile pour voir si le facteur limitant en démo est le flou, l'incohérence, ou la confiance — voir `docs/LIMITATIONS.md`). - `image_quality_distribution` : histogramme des `image_quality` observés. Deux façons de le lire : ```bash # en ligne de commande, pour un rapport texte cd yely_ai_module python -m monitoring.metrics # via l'API elle-même, pendant la démo curl http://127.0.0.1:8000/metrics ``` `GET /metrics` renvoie le même dictionnaire que `compute_metrics()`, sans paramètre — il relit `logs/api.log` à chaque appel, donc reflète l'état courant sans redémarrer le serveur. `web/stats.html` est un tableau de bord minimal pour la soutenance : il appelle `GET /metrics` (URL de l'API dérivée de celle configurée dans `web/index.html`, stockée dans le même `localStorage`) et affiche requêtes totales, taux de succès, confiance moyenne, répartition qualité image et causes de blocage, avec auto-rafraîchissement toutes les 15s. Nécessite que `allow_methods` inclue `GET` dans le middleware CORS de `app/main.py` (sinon le navigateur bloque la lecture de la réponse, même si l'appel aboutit côté serveur — piège déjà rencontré avec `/analyze` en `POST`). `model_version` (variable d'environnement `MODEL_VERSION`, valeur par défaut `crnn_v2_70.2pct`) est ajouté à chaque entrée de log — pas à la réponse publique de `/analyze`, pour ne pas polluer le contrat API §13 avec un champ technique. Il permet de comparer `logs/api.log` avant/après un ré-entraînement en filtrant par version, une fois plusieurs modèles utilisés en séquence (voir §3). ## 2. Boucle d'apprentissage continu (`monitoring/feedback.py`) ### Principe Chaque photo reçue par `/analyze` est déjà sauvegardée dans `photos/.jpg` et référencée dans la réponse (`photo_reference`). Cette boucle réutilise cette photo pour transformer une lecture erronée, corrigée a posteriori par un pompiste ou un contrôle YELY, en nouvelle donnée d'entraînement — sans jamais toucher au dataset pendant qu'un entraînement est en cours (leçon retenue d'un crash réel, voir `docs/TRAINING.md`). ### Étape 1 — collecte : `POST /feedback` ```bash curl -X POST http://127.0.0.1:8000/feedback \ -F "photo_reference=3f2a1c9e-....jpg" \ -F "corrected_prix=10000" \ -F "corrected_volume=14.28" \ -F "corrected_prix_litre=700" \ -F "corrected_by=pompiste-7" ``` - `photo_reference` (requis) : valeur renvoyée par `/analyze` dans la réponse d'origine — l'API vérifie que la photo existe encore dans `photos/` avant d'accepter la correction (`404` sinon). - Au moins un des trois champs `corrected_prix` / `corrected_volume` / `corrected_prix_litre` est requis ; les champs non fournis restent inconnus (pas déduits automatiquement). - Chaque correction est ajoutée en une ligne à `monitoring/feedback.jsonl` (append-only, jamais réécrit sur cet endpoint) via `monitoring.feedback.record_feedback`. **En pratique (démo/soutenance), pas besoin de `curl`** : `web/stats.html` liste les transactions bloquées (`GET /failures`, 20 dernières, avec la photo servie par `GET /photos/{photo_reference}`) avec un bouton « Corriger » par entrée, qui ouvre `web/feedback.html?ref=` — la référence est déjà pré-remplie, la photo s'affiche automatiquement, il ne reste qu'à saisir les valeurs correctes et valider. Cette page reste aussi utilisable seule (référence saisie à la main) si besoin. ### Étape 2 — conversion : `monitoring/feedback.py` ```bash cd yely_ai_module python -m monitoring.feedback ``` Pour chaque entrée de `feedback.jsonl` pas encore convertie : 1. Relit la photo dans `photos/`. 2. Relance `detect_screen_region` (même fonction que l'API) pour localiser l'écran et produire un crop, sauvegardé dans `monitoring/feedback_crops/`. 3. Ajoute une entrée à `annotator/annotations/annotations.json`, **au même format** que l'annotation manuelle (`lcd_bbox`, `lcd_crop`, `fields`), en utilisant les valeurs corrigées comme champs. 4. Marque l'entrée `converted=true` dans `feedback.jsonl` pour ne pas la reconvertir au prochain passage. **Point important** : la détection d'écran automatique n'est pas fiable à 100% (voir `docs/LIMITATIONS.md`, point 2 — 86% des images d'origine sont tombées sur un repli approximatif). Les entrées générées ici sont donc marquées `"status": "pending_review"` (pas `"annotated"`) : une relecture humaine rapide via l'outil `annotator/` (vérifier que `lcd_bbox` cadre bien l'écran) reste nécessaire avant de les inclure dans un ré-entraînement. Ce choix est délibéré — préférer une étape manuelle courte à l'injection silencieuse de crops mal cadrés dans le dataset, qui dégraderait la précision plutôt que de l'améliorer (cf. la cause n°2 de la limitation à 70.2%). #### Relire les entrées `pending_review` (`annotator/annotate.py --review-pending`) `annotate.py` a un mode dédié qui ne parcourt pas un dossier d'images mais relit directement les entrées `status="pending_review"` d'`annotations.json` — une par une, avec le rectangle et les valeurs déjà pré-remplis (à confirmer ou corriger) au lieu de repartir de zéro : ```bash cd annotator python annotate.py --review-pending ``` Marche à suivre pour chaque image affichée : 1. Le rectangle vert affiché est le `lcd_bbox` détecté automatiquement au moment de la conversion — regarde s'il cadre bien l'écran LCD. 2. **S'il est correct** : appuie sur `S` ou `Entrée` directement, pas besoin de redessiner. 3. **S'il est mal cadré** (coupe un chiffre, déborde sur le boîtier...) : appuie sur `R` pour l'effacer, puis dessine un nouveau rectangle (clic + glisser) avant `S`/`Entrée`. 4. Le terminal demande ensuite de confirmer les valeurs (prix, volume, prix/litre...) — elles sont **déjà pré-remplies** avec la correction envoyée par le pompiste (visibles entre crochets `[...]`) : appuie sur `Entrée` pour chaque champ correct, ou retape la valeur si besoin de l'ajuster. 5. `N` pour ignorer une entrée douteuse (photo illisible, correction qui ne semble pas fiable) — elle passe en `status="skipped"` et ne sera plus proposée ni utilisée à l'entraînement. 6. `Q`/`Esc` pour arrêter la session en cours ; la progression déjà validée est sauvegardée, il suffit de relancer `--review-pending` plus tard pour reprendre (seules les entrées encore `pending_review` sont reproposées). Chaque entrée confirmée passe automatiquement en `status="annotated"` — la suite (§3) la traite alors exactement comme une annotation manuelle classique. `python annotate.py --review` (sans `--review-pending`) affiche à tout moment un résumé indiquant combien d'entrées restent `[à relire]`. ### Étape 3 — ré-entraînement périodique Une fois un nombre suffisant d'entrées `pending_review` relues et passées à `"annotated"` (ex. tous les 30-50 nouvelles corrections, à ajuster selon le volume réel observé en production) : ```bash cd annotator python split_lines.py # nettoie et régénère train/val (fix leak inclus) cd ../train python prepare_doctr_dataset.py python finetune_doctr.py --epochs 60 ``` Voir `docs/TRAINING.md` pour le détail de chaque étape et les pièges déjà rencontrés (ne pas modifier le dataset pendant l'entraînement, toujours repartir d'un dossier de sortie nettoyé). ### Étape 4 — versioning du modèle actif Chaque nouveau modèle entraîné est écrit dans son propre dossier (`train/models/v/`), jamais en écrasant le précédent — l'incident de ce projet où un entraînement de test a écrasé le meilleur checkpoint (55.3%) avec un résultat plus faible (44.7%) a motivé ce choix. Pour déployer une nouvelle version : 1. Copier le `.pt` retenu vers `yely_ai_module/models/crnn_fuel_pump_best.pt` (écrase le fichier servi par l'API, mais l'ancien reste dans `train/models/v/` si un retour arrière est nécessaire). 2. Mettre à jour `MODEL_VERSION` (variable d'environnement du déploiement, ex. Hugging Face Spaces → Settings → Variables) pour que les nouvelles entrées de `logs/api.log` soient distinguables des précédentes dans `monitoring/metrics.py`. 3. Redémarrer le Space (ou le processus `uvicorn`) — le modèle est chargé une seule fois au démarrage (singleton `_get_model()`), un nouveau fichier sur disque n'est pas repris à chaud. ## 3. Stockage persistant (obligatoire pour que tout ceci survive en production) Par défaut, un Hugging Face Space Docker écrit sur le disque **éphémère** du conteneur : `photos/`, `logs/api.log` et `monitoring/feedback.jsonl` disparaissent à chaque redémarrage/veille du Space (les Spaces gratuits se mettent en veille après une période d'inactivité). Pour une démo tenue en une seule session continue ce n'est pas gênant, mais **la boucle de feedback et l'historique `/metrics`/`/failures` n'ont de sens que si ces fichiers survivent** — c'est là qu'intervient le stockage persistant. ### Ce que fait le code `app/main.py`, `monitoring/metrics.py` et `monitoring/feedback.py` lisent tous la même variable d'environnement `YELY_DATA_DIR` : si elle est définie, `photos/`, `logs/` et `monitoring/feedback.jsonl` sont placés sous ce répertoire au lieu du dossier de l'application. Par défaut (variable absente), tout reste sous `yely_ai_module/` comme avant — donc rien ne casse en local ou sur un Space sans stockage persistant. ### Activer le stockage persistant sur le Space 1. Ouvre ton Space → **Settings** → section **Persistent storage**. C'est une option payante (facturée au mois, plusieurs tailles proposées) — vérifie le tarif affiché à cet instant sur la page, il peut avoir changé. Pour une démo, la plus petite taille suffit largement (quelques photos + logs texte). 2. Active-la et choisis une taille. Hugging Face monte un disque dans le conteneur — le chemin de montage est indiqué dans l'interface au moment de l'activation (généralement `/data`). 3. Toujours dans **Settings** → **Variables and secrets** → **New variable** (pas *secret*, cette valeur n'a rien de sensible) : - Nom : `YELY_DATA_DIR` - Valeur : le chemin de montage indiqué à l'étape 2 (ex. `/data`) 4. Redémarre le Space (**Settings** → **Restart this Space**, ou un simple push suffit à le redéployer) pour que la variable soit prise en compte. 5. **Vérifier que ça persiste vraiment** : envoie une image via `/analyze` (ou le formulaire `web/index.html`), consulte `GET /metrics` (`total_requests` ≥ 1), puis redémarre le Space depuis Settings et rappelle `GET /metrics` — si `total_requests` n'est pas retombé à 0, c'est branché correctement. ### Si tu ne veux pas payer pour la démo Pas bloquant : sans stockage persistant, tout fonctionne normalement tant que le Space ne redémarre pas pendant la session de démonstration (soutenance en continu). C'est seulement l'historique qui ne survit pas d'une session à l'autre — acceptable pour une démo ponctuelle, à mentionner comme limitation connue si le sujet est posé (`docs/LIMITATIONS.md`). ## 4. Ce qui est réellement implémenté vs conçu Fonctionnel et testé (`tests/test_monitoring.py`) : `POST /feedback`, `GET /metrics`, `GET /failures`, `GET /photos/{photo_reference}`, `monitoring/metrics.py`, `monitoring/feedback.py::record_feedback` et `::convert_feedback_to_annotations`. Le mode `annotate.py --review-pending` n'a pas de test automatisé (outil interactif OpenCV, nécessite un affichage) — vérifié manuellement. Conçu mais volontairement manuel (pas automatisé, par choix — voir §2 ci-dessus) : la relecture des entrées `pending_review` et le déclenchement du ré-entraînement lui-même, qui restent des actions humaines délibérées plutôt qu'un cron — le volume de données actuel (quelques centaines d'images) ne justifie pas encore une automatisation complète, et une relecture humaine reste la meilleure garde-fou contre la dégradation du dataset tant que le découpage en lignes n'est pas plus robuste.