6.0 KiB
6.0 KiB
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 :
- Photographier une plante inconnue → l'app propose une identification
- Associer la photo à une plante du catalogue (existante ou créée)
- 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)
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
- Zone drag & drop /
<input type="file" accept="image/*" capture="camera"> - Upload → POST
/api/identify→ spinner Gruvbox - Affichage top-3 : nom commun, nom latin, barre de confiance colorée
- Actions :
- Associer à une plante existante (select dropdown)
- Créer cette plante (pré-remplit nom_commun + famille)
- Ignorer (enregistre la photo sans identification)
- Photo sauvegardée dans
Mediaavecidentified_*champs renseignés
Composant réutilisable : PhotoGallery.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 versai-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
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 fallbacktests/test_media_enriched.py: CRUD avec champs identified_*
Ordre d'implémentation suggéré
- ai-service Docker (FastAPI + YOLO endpoint /detect)
- Redis container + redis_cache.py service
- plantnet.py service + yolo_service.py service
- Endpoint
/api/identifydans le backend - Migration Media (champs identified_*)
PhotoGallery.vuecomposant réutilisablePhotoIdentifyModal.vueBibliothequeView.vue+ route/bibliotheque- Intégration dans la navigation (sidebar)
- Tests backend