# 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 / `` 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 ``` - 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