File size: 13,168 Bytes
3020394
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
# 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.