Spaces:
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éponsessuccess=true.avg_confidence_score: moyenne desconfidence_scorerenvoyés.blocking_causes: histogramme desmessagepour les réponses bloquées (utile pour voir si le facteur limitant en démo est le flou, l'incohérence, ou la confiance — voirdocs/LIMITATIONS.md).image_quality_distribution: histogramme desimage_qualityobservés.
Deux façons de le lire :
# 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
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/analyzedans la réponse d'origine — l'API vérifie que la photo existe encore dansphotos/avant d'accepter la correction (404sinon).- Au moins un des trois champs
corrected_prix/corrected_volume/corrected_prix_litreest 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) viamonitoring.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
cd yely_ai_module
python -m monitoring.feedback
Pour chaque entrée de feedback.jsonl pas encore convertie :
- Relit la photo dans
photos/. - Relance
detect_screen_region(même fonction que l'API) pour localiser l'écran et produire un crop, sauvegardé dansmonitoring/feedback_crops/. - 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. - Marque l'entrée
converted=truedansfeedback.jsonlpour 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 :
cd annotator
python annotate.py --review-pending
Marche à suivre pour chaque image affichée :
- Le rectangle vert affiché est le
lcd_bboxdétecté automatiquement au moment de la conversion — regarde s'il cadre bien l'écran LCD. - S'il est correct : appuie sur
SouEntréedirectement, pas besoin de redessiner. - S'il est mal cadré (coupe un chiffre, déborde sur le boîtier...) :
appuie sur
Rpour l'effacer, puis dessine un nouveau rectangle (clic + glisser) avantS/Entrée. - 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 surEntréepour chaque champ correct, ou retape la valeur si besoin de l'ajuster. Npour ignorer une entrée douteuse (photo illisible, correction qui ne semble pas fiable) — elle passe enstatus="skipped"et ne sera plus proposée ni utilisée à l'entraînement.Q/Escpour arrêter la session en cours ; la progression déjà validée est sauvegardée, il suffit de relancer--review-pendingplus tard pour reprendre (seules les entrées encorepending_reviewsont 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) :
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 :
- Copier le
.ptretenu versyely_ai_module/models/crnn_fuel_pump_best.pt(écrase le fichier servi par l'API, mais l'ancien reste danstrain/models/v<N>/si un retour arrière est nécessaire). - Mettre à jour
MODEL_VERSION(variable d'environnement du déploiement, ex. Hugging Face Spaces → Settings → Variables) pour que les nouvelles entrées delogs/api.logsoient distinguables des précédentes dansmonitoring/metrics.py. - 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
- 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).
- 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). - 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)
- Nom :
- Redémarre le Space (Settings → Restart this Space, ou un simple push suffit à le redéployer) pour que la variable soit prise en compte.
- Vérifier que ça persiste vraiment : envoie une image via
/analyze(ou le formulaireweb/index.html), consulteGET /metrics(total_requests≥ 1), puis redémarre le Space depuis Settings et rappelleGET /metrics— sitotal_requestsn'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.