Files
jardin/docs/plans/2026-02-22-bibliotheque-photo-design.md
2026-02-22 15:05:40 +01:00

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 :

  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)

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

<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

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