From 560fa63a45480ad2045e0f7cc6245d289f02e2af Mon Sep 17 00:00:00 2001 From: gilles Date: Sun, 22 Feb 2026 12:18:56 +0100 Subject: [PATCH] feat(backend): endpoint POST /api/identify PlantNet + YOLO fallback + cache Co-Authored-By: Claude Sonnet 4.6 --- backend/app/main.py | 34 ++++++++++++++++++++++++++++++--- backend/app/routers/identify.py | 29 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 backend/app/routers/identify.py diff --git a/backend/app/main.py b/backend/app/main.py index 43d1bbf..37a831c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -11,7 +11,13 @@ from app.database import create_db_and_tables @asynccontextmanager async def lifespan(app: FastAPI): os.makedirs(UPLOAD_DIR, exist_ok=True) + try: + os.makedirs("/data/skyfield", exist_ok=True) + except OSError: + pass import app.models # noqa — enregistre tous les modèles avant create_all + from app.migrate import run_migrations + run_migrations() create_db_and_tables() from app.seed import run_seed run_seed() @@ -27,14 +33,35 @@ app.add_middleware( allow_headers=["*"], ) -from app.routers import gardens, varieties, plantings, tasks, settings, media +from app.routers import ( # noqa + gardens, + plants, + plantings, + tasks, + settings, + media, + tools, + dictons, + astuces, + recoltes, + lunar, + meteo, + identify, +) app.include_router(gardens.router, prefix="/api") -app.include_router(varieties.router, prefix="/api") +app.include_router(plants.router, prefix="/api") app.include_router(plantings.router, prefix="/api") app.include_router(tasks.router, prefix="/api") app.include_router(settings.router, prefix="/api") app.include_router(media.router, prefix="/api") +app.include_router(tools.router, prefix="/api") +app.include_router(dictons.router, prefix="/api") +app.include_router(astuces.router, prefix="/api") +app.include_router(recoltes.router, prefix="/api") +app.include_router(lunar.router, prefix="/api") +app.include_router(meteo.router, prefix="/api") +app.include_router(identify.router, prefix="/api") @app.get("/api/health") @@ -43,6 +70,7 @@ def health(): # Monter uploads seulement si le dossier existe -from fastapi.staticfiles import StaticFiles +from fastapi.staticfiles import StaticFiles # noqa + if os.path.isdir(UPLOAD_DIR): app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR, html=False), name="uploads") diff --git a/backend/app/routers/identify.py b/backend/app/routers/identify.py new file mode 100644 index 0000000..f668c93 --- /dev/null +++ b/backend/app/routers/identify.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter, File, UploadFile +from app.services import plantnet, yolo_service, redis_cache + +router = APIRouter(tags=["identification"]) + + +@router.post("/identify") +async def identify_plant(file: UploadFile = File(...)): + image_bytes = await file.read() + + # 1. Cache Redis + cached = redis_cache.get(image_bytes) + if cached is not None: + return {"source": "cache", "results": cached} + + # 2. PlantNet (cloud) + results = await plantnet.identify(image_bytes, file.filename or "photo.jpg") + + # 3. Fallback YOLO si PlantNet indisponible + if not results: + results = await yolo_service.identify(image_bytes) + source = "yolo" + else: + source = "plantnet" + + # 4. Mettre en cache + redis_cache.set(image_bytes, results) + + return {"source": source, "results": results}