Spaces:
Sleeping
Sleeping
| """ | |
| FarmGuard Nigeria - Plant Disease Detection API | |
| Using Google Gemini Vision for accurate real photo analysis | |
| """ | |
| import json | |
| import logging | |
| import os | |
| import base64 | |
| from google import genai | |
| from google.genai import types | |
| from dotenv import load_dotenv | |
| from fastapi import FastAPI, File, UploadFile, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| # Load .env file | |
| load_dotenv() | |
| # Setup logging | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI( | |
| title="FarmGuard Nigeria API", | |
| description="AI-powered plant disease detection for Nigerian farmers", | |
| version="2.0.0" | |
| ) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Load disease config | |
| config_path = os.path.join(os.path.dirname(__file__), "disease_config.json") | |
| with open(config_path) as f: | |
| DISEASE_CONFIG = json.load(f) | |
| # Setup Gemini | |
| GEMINI_KEY = os.getenv("GEMINI_API_KEY") | |
| gemini_client = None | |
| if GEMINI_KEY: | |
| gemini_client = genai.Client(api_key=GEMINI_KEY) | |
| logger.info("Gemini Vision API ready") | |
| else: | |
| logger.warning("No GEMINI_API_KEY found in .env file") | |
| def root(): | |
| return { | |
| "message": "FarmGuard Nigeria API is running!", | |
| "version": "2.0.0", | |
| "gemini_ready": gemini_client is not None, | |
| "docs": "/docs" | |
| } | |
| def health(): | |
| return { | |
| "status": "healthy", | |
| "gemini_ready": gemini_client is not None, | |
| } | |
| async def predict(file: UploadFile = File(...)): | |
| """Detect plant disease from uploaded leaf image using Gemini Vision.""" | |
| if file.content_type not in ["image/jpeg", "image/jpg", "image/png"]: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Only JPEG and PNG images are supported" | |
| ) | |
| image_bytes = await file.read() | |
| if len(image_bytes) > 8 * 1024 * 1024: | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Image too large. Maximum size is 8MB" | |
| ) | |
| if gemini_client is None: | |
| raise HTTPException( | |
| status_code=503, | |
| detail="Gemini API not configured. Add GEMINI_API_KEY to your .env file" | |
| ) | |
| try: | |
| mime_type = file.content_type | |
| prompt = """You are an expert Nigerian agricultural plant pathologist. | |
| Analyse this plant leaf image and respond ONLY with a JSON object in this exact format with no extra text: | |
| { | |
| "crop_identified": true, | |
| "plant": "crop name e.g. Tomato, Maize, Cassava, Potato, Pepper, Yam, Plantain", | |
| "disease": "disease name or Healthy", | |
| "is_healthy": false, | |
| "confidence": 0.85, | |
| "severity": "none or low or moderate or high or very_high", | |
| "symptoms": ["symptom 1", "symptom 2", "symptom 3"], | |
| "causes": "what causes this disease", | |
| "treatment": { | |
| "chemical": "specific chemical treatment with Nigerian product names e.g. Dithane M-45, Cobox, Ridomil Gold, Confidor", | |
| "cultural": "cultural and physical management practices", | |
| "preventive": "prevention measures for Nigerian farmers" | |
| }, | |
| "urgency": "treatment urgency description", | |
| "economic_impact": "potential yield and economic impact on Nigerian farmer", | |
| "message": "" | |
| } | |
| Rules: | |
| - If you cannot see a plant leaf clearly, set crop_identified to false and explain in message | |
| - Focus on crops grown in Nigeria: Tomato, Maize, Cassava, Potato, Pepper, Yam, Plantain, Rice, Cowpea, Soybean, Orange, Groundnut, Banana | |
| - Always include locally available Nigerian product names in chemical treatment | |
| - Be specific and practical for Nigerian smallholder farmers | |
| - Respond with ONLY the JSON object, nothing else""" | |
| # Call Gemini with new SDK | |
| response = gemini_client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=[ | |
| types.Part.from_bytes(data=image_bytes, mime_type=mime_type), | |
| prompt | |
| ] | |
| ) | |
| text = response.text.strip() | |
| # Remove markdown code blocks if present | |
| if "```json" in text: | |
| text = text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in text: | |
| text = text.split("```")[1].split("```")[0].strip() | |
| result = json.loads(text) | |
| # Handle unidentified image | |
| if not result.get("crop_identified", False): | |
| return { | |
| "success": False, | |
| "crop_identified": False, | |
| "message": result.get("message", "Could not identify a plant leaf. Please upload a clear close-up photo of a single leaf."), | |
| "top_prediction": None, | |
| "all_predictions": [], | |
| "is_healthy": False | |
| } | |
| severity = result.get("severity", "moderate") | |
| severity_colors = { | |
| "none": "#16a34a", "low": "#ca8a04", | |
| "moderate": "#ea580c", "high": "#dc2626", "very_high": "#7f1d1d" | |
| } | |
| top_prediction = { | |
| "plant": result.get("plant", "Unknown"), | |
| "disease": result.get("disease", "Unknown"), | |
| "confidence": float(result.get("confidence", 0.8)), | |
| "severity": severity, | |
| "severity_color": severity_colors.get(severity, "#ea580c"), | |
| "symptoms": result.get("symptoms", []), | |
| "causes": result.get("causes", ""), | |
| "treatment": result.get("treatment", {}), | |
| "urgency": result.get("urgency", ""), | |
| "economic_impact": result.get("economic_impact", "") | |
| } | |
| conf = float(result.get("confidence", 0.8)) | |
| confidence_level = "high" if conf >= 0.7 else "medium" if conf >= 0.4 else "low" | |
| return { | |
| "success": True, | |
| "crop_identified": True, | |
| "is_healthy": result.get("is_healthy", False), | |
| "top_prediction": top_prediction, | |
| "all_predictions": [top_prediction], | |
| "confidence_level": confidence_level, | |
| "message": None | |
| } | |
| except json.JSONDecodeError as e: | |
| logger.error(f"JSON parse error: {e}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail="Failed to parse AI response. Please try again." | |
| ) | |
| except Exception as e: | |
| logger.error(f"Gemini error: {e}") | |
| raise HTTPException( | |
| status_code=500, | |
| detail=f"AI analysis failed: {str(e)}" | |
| ) | |
| def recommend(state: str = "Rivers", soil: str = "loamy", season: str = "rainy"): | |
| """Get crop recommendations based on location, soil and season.""" | |
| recommendations = { | |
| "rainy": { | |
| "crops": ["Cassava", "Yam", "Maize", "Plantain", "Cocoyam", "Waterleaf", "Fluted Pumpkin (Ugu)"], | |
| "tip": "Rainy season is ideal for root crops and leafy vegetables. Ensure good drainage to prevent waterlogging." | |
| }, | |
| "dry": { | |
| "crops": ["Tomato", "Pepper", "Onion", "Carrot", "Cabbage", "Cowpea", "Soybean", "Groundnut"], | |
| "tip": "Dry season farming requires irrigation. Vegetables thrive well with consistent water supply." | |
| } | |
| } | |
| soil_tips = { | |
| "loamy": "Loamy soil is excellent and suitable for almost all Nigerian crops. Maintain organic matter with compost.", | |
| "sandy": "Sandy soil drains fast — best for cassava, groundnut and sweet potato. Add organic matter to improve water retention.", | |
| "clay": "Clay soil retains water well — suitable for rice and yam. Ensure adequate drainage ridges to avoid waterlogging." | |
| } | |
| season_data = recommendations.get(season.lower(), recommendations["rainy"]) | |
| soil_tip = soil_tips.get(soil.lower(), "Prepare soil well with compost before planting.") | |
| return { | |
| "state": state, | |
| "season": season, | |
| "soil_type": soil, | |
| "recommended_crops": season_data["crops"], | |
| "season_tip": season_data["tip"], | |
| "soil_tip": soil_tip | |
| } | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) | |