Spaces:
Sleeping
Sleeping
| # 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/<transaction_id>.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=<photo_reference>` | |
| — 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<N>/`), 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<N>/` | |
| 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. | |