avant codex
This commit is contained in:
238
docs/plans/2026-02-22-bibliotheque-photo-design.md
Normal file
238
docs/plans/2026-02-22-bibliotheque-photo-design.md
Normal file
@@ -0,0 +1,238 @@
|
||||
# Design — Bibliothèque photo & Identification de plantes
|
||||
|
||||
**Date** : 2026-02-22
|
||||
**Statut** : Approuvé
|
||||
|
||||
---
|
||||
|
||||
## Objectif
|
||||
|
||||
Ajouter une bibliothèque photo centralisée à l'application jardin, couplée à une
|
||||
identification automatique de plantes par photo. L'utilisateur peut :
|
||||
|
||||
1. Photographier une plante inconnue → l'app propose une identification
|
||||
2. Associer la photo à une plante du catalogue (existante ou créée)
|
||||
3. Consulter toutes ses photos depuis une galerie globale ou depuis chaque fiche
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
[iPhone / navigateur]
|
||||
│ POST /api/identify (image multipart)
|
||||
▼
|
||||
[backend FastAPI]
|
||||
│ 1. SHA256(image) → vérifier Redis (TTL 7j)
|
||||
│ └─ cache hit → réponse immédiate
|
||||
│ 2. cache miss → PlantNet API (cloud, clé API configurée)
|
||||
│ └─ si timeout/erreur → appel interne ai-service
|
||||
│ 3. stocker résultat dans Redis
|
||||
└──► JSON top-3 espèces identifiées
|
||||
|
||||
Services Docker :
|
||||
backend : FastAPI existant (port 8060) + redis-py
|
||||
ai-service : FastAPI minimal + ultralytics YOLOv8 (port 8070, réseau interne)
|
||||
redis : redis:alpine (port 6379 interne uniquement)
|
||||
frontend : nginx existant (port 8061)
|
||||
|
||||
Volumes :
|
||||
db : SQLite (existant)
|
||||
uploads : fichiers media (existant)
|
||||
yolo_models : cache modèle YOLO foduucom/plant-leaf-detection-and-classification
|
||||
redis_data : persistance Redis
|
||||
```
|
||||
|
||||
**Clé PlantNet** : `2b1088cHCJ4c7Cn2Vqq67xfve`
|
||||
**Modèle YOLO** : `foduucom/plant-leaf-detection-and-classification` (46 classes de feuilles)
|
||||
|
||||
---
|
||||
|
||||
## Modèle de données
|
||||
|
||||
### Media (enrichi)
|
||||
|
||||
```python
|
||||
class Media(SQLModel, table=True):
|
||||
id: Optional[int]
|
||||
entity_type: str # jardin|plante|outil|plantation
|
||||
entity_id: int
|
||||
url: str
|
||||
thumbnail_url: Optional[str]
|
||||
titre: Optional[str]
|
||||
# Nouveaux champs identification
|
||||
identified_species: Optional[str] # "Solanum lycopersicum"
|
||||
identified_common: Optional[str] # "Tomate"
|
||||
identified_confidence: Optional[float] # 0.94
|
||||
identified_source: Optional[str] # "plantnet" | "yolo" | "cache"
|
||||
created_at: datetime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API
|
||||
|
||||
### Identification
|
||||
|
||||
```
|
||||
POST /api/identify
|
||||
Content-Type: multipart/form-data
|
||||
Body: file (image)
|
||||
|
||||
Réponse 200:
|
||||
{
|
||||
"source": "plantnet" | "yolo" | "cache",
|
||||
"results": [
|
||||
{
|
||||
"species": "Solanum lycopersicum",
|
||||
"common_name": "Tomate",
|
||||
"confidence": 0.94,
|
||||
"image_url": "https://..." # optionnel, depuis PlantNet
|
||||
},
|
||||
... # jusqu'à 3 résultats
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### ai-service interne
|
||||
|
||||
```
|
||||
POST /detect (interne, port 8070)
|
||||
Content-Type: multipart/form-data
|
||||
Body: file (image)
|
||||
|
||||
Réponse 200:
|
||||
[
|
||||
{ "class_name": "Tomato___healthy", "confidence": 0.87 },
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
### Media enrichi
|
||||
|
||||
```
|
||||
GET /api/media?entity_type=plante&entity_id=1
|
||||
POST /api/media { entity_type, entity_id, url, thumbnail_url, identified_species, ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
|
||||
### Nouvelle page : BibliothequeView.vue
|
||||
|
||||
- Route : `/bibliotheque`
|
||||
- Grille masonry de miniatures (toutes photos `Media`)
|
||||
- Filtres par `entity_type` : Toutes | Plantes | Jardins | Plantations | Outils
|
||||
- Bouton **"Identifier une plante"** (ouvre PhotoIdentifyModal)
|
||||
- Clic miniature → navigation vers la fiche liée
|
||||
|
||||
### Modal d'identification : PhotoIdentifyModal.vue
|
||||
|
||||
1. Zone drag & drop / `<input type="file" accept="image/*" capture="camera">`
|
||||
2. Upload → POST `/api/identify` → spinner Gruvbox
|
||||
3. Affichage top-3 : nom commun, nom latin, barre de confiance colorée
|
||||
4. Actions :
|
||||
- **Associer** à une plante existante (select dropdown)
|
||||
- **Créer cette plante** (pré-remplit nom_commun + famille)
|
||||
- **Ignorer** (enregistre la photo sans identification)
|
||||
5. Photo sauvegardée dans `Media` avec `identified_*` champs renseignés
|
||||
|
||||
### Composant réutilisable : PhotoGallery.vue
|
||||
|
||||
```vue
|
||||
<PhotoGallery entity-type="plante" :entity-id="plant.id" />
|
||||
```
|
||||
|
||||
- Charge `GET /api/media?entity_type=X&entity_id=Y`
|
||||
- Grille de miniatures, lightbox au clic
|
||||
- Bouton "Ajouter une photo" → upload in-place → optionnel : identification
|
||||
|
||||
### Navigation
|
||||
|
||||
Ajouter "Bibliothèque" dans le sidebar (AppDrawer + App.vue desktop) après "Plantes".
|
||||
|
||||
---
|
||||
|
||||
## Services
|
||||
|
||||
### backend/app/services/plantnet.py
|
||||
|
||||
- `identify(image_bytes) -> list[dict]` : appel HTTPS PlantNet `/v2/identify`
|
||||
- Timeout 10s, retourne `[]` si erreur
|
||||
|
||||
### backend/app/services/yolo_service.py
|
||||
|
||||
- `identify(image_bytes) -> list[dict]` : POST HTTP vers `ai-service:8070/detect`
|
||||
- Timeout 30s (inférence CPU peut être lente)
|
||||
- Mappe `class_name` → nom commun français
|
||||
|
||||
### backend/app/services/redis_cache.py
|
||||
|
||||
- `get(key: str) -> Optional[list]`
|
||||
- `set(key: str, value: list, ttl: int = 604800)` (7 jours)
|
||||
- Clé = `f"identify:{sha256(image_bytes).hexdigest()}"`
|
||||
|
||||
---
|
||||
|
||||
## ai-service Docker
|
||||
|
||||
```
|
||||
ai-service/
|
||||
Dockerfile
|
||||
main.py # FastAPI minimal
|
||||
requirements.txt # fastapi uvicorn ultralytics pillow python-multipart
|
||||
```
|
||||
|
||||
- `POST /detect` : charge le modèle lazy au 1er appel, cache en mémoire
|
||||
- Modèle téléchargé dans `/models` (volume Docker persistant)
|
||||
- Pas d'exposition externe (réseau Docker interne uniquement)
|
||||
|
||||
---
|
||||
|
||||
## docker-compose additions
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ai-service:
|
||||
build: ./ai-service
|
||||
volumes:
|
||||
- yolo_models:/models
|
||||
environment:
|
||||
- MODEL_CACHE_DIR=/models
|
||||
networks:
|
||||
- jardin-net
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
networks:
|
||||
- jardin-net
|
||||
|
||||
volumes:
|
||||
yolo_models:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/test_identify.py` : mock PlantNet + mock ai-service, vérifier fallback
|
||||
- `tests/test_media_enriched.py` : CRUD avec champs identified_*
|
||||
|
||||
---
|
||||
|
||||
## Ordre d'implémentation suggéré
|
||||
|
||||
1. ai-service Docker (FastAPI + YOLO endpoint /detect)
|
||||
2. Redis container + redis_cache.py service
|
||||
3. plantnet.py service + yolo_service.py service
|
||||
4. Endpoint `/api/identify` dans le backend
|
||||
5. Migration Media (champs identified_*)
|
||||
6. `PhotoGallery.vue` composant réutilisable
|
||||
7. `PhotoIdentifyModal.vue`
|
||||
8. `BibliothequeView.vue` + route `/bibliotheque`
|
||||
9. Intégration dans la navigation (sidebar)
|
||||
10. Tests backend
|
||||
1077
docs/plans/2026-02-22-bibliotheque-photo-plan.md
Normal file
1077
docs/plans/2026-02-22-bibliotheque-photo-plan.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user