Spaces:
Running
Running
Fadhili Sumaye
Implement admin password page, database indexing, and optimize app lifecycle polling
436892e | from fastapi import FastAPI, File, UploadFile, HTTPException, Request, Form | |
| from fastapi.responses import PlainTextResponse, HTMLResponse, FileResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import os | |
| from pathlib import Path | |
| import threading | |
| import time | |
| import urllib.request | |
| import sqlite3 | |
| import ipaddress | |
| import json | |
| import io | |
| from PIL import Image | |
| from typing import Optional | |
| from collections import defaultdict | |
| BASE_DIR = Path(__file__).resolve().parent | |
| DB_FILE = BASE_DIR / "pest_detection.db" | |
| MODEL_FILE = BASE_DIR / "best_cereal.pt" | |
| MODEL_IP102_FILE = BASE_DIR / "best_ip102.pt" | |
| IP102_URL = "https://huggingface.co/underdogquality/yolo11s-pest-detection/resolve/main/best.pt" | |
| def init_db(): | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| cursor = conn.cursor() | |
| try: | |
| cursor.execute("PRAGMA journal_mode=WAL;") | |
| except Exception as e: | |
| print(f"[DB] Warning: Failed to set WAL mode: {e}") | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS audit_logs ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| username TEXT, | |
| timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, | |
| endpoint TEXT NOT NULL, | |
| status TEXT NOT NULL, | |
| details TEXT, | |
| ip_address TEXT | |
| ) | |
| """) | |
| cursor.execute(""" | |
| CREATE TABLE IF NOT EXISTS unrecognized_reports ( | |
| id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| device_id TEXT NOT NULL, | |
| image_path TEXT NOT NULL, | |
| status TEXT DEFAULT 'pending', | |
| pest_name TEXT, | |
| treatment TEXT, | |
| created_at DATETIME DEFAULT CURRENT_TIMESTAMP | |
| ) | |
| """) | |
| cursor.execute("CREATE INDEX IF NOT EXISTS idx_unrecognized_reports_device_id ON unrecognized_reports (device_id);") | |
| conn.commit() | |
| # Prune audit logs older than 30 days | |
| try: | |
| cursor.execute("DELETE FROM audit_logs WHERE timestamp < datetime('now', '-30 days')") | |
| pruned_count = cursor.rowcount | |
| conn.commit() | |
| if pruned_count > 0: | |
| print(f"[DB] Successfully pruned {pruned_count} audit log entries older than 30 days.") | |
| except Exception as e: | |
| print(f"[DB] Warning: Failed to prune audit logs: {e}") | |
| conn.close() | |
| def log_audit(username: Optional[str], endpoint: str, status: str, details: str, ip_address: str): | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "INSERT INTO audit_logs (username, endpoint, status, details, ip_address) VALUES (?, ?, ?, ?, ?)", | |
| (username, endpoint, status, details, ip_address) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| except Exception as e: | |
| print(f"Failed to write audit log: {e}") | |
| init_db() | |
| from fastapi import Depends, Header, Query | |
| def authenticate_admin( | |
| x_admin_password: Optional[str] = Header(None), | |
| password: Optional[str] = Query(None) | |
| ): | |
| admin_pass = os.environ.get("ADMIN_PASSWORD", "admin123") | |
| passed_pass = x_admin_password or password | |
| if not passed_pass or passed_pass != admin_pass: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="Unauthorized: Invalid admin password" | |
| ) | |
| return True | |
| app = FastAPI(title="Pest Detection API") | |
| # Configure CORS Middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # In-memory IP-based rate limiting | |
| RATE_LIMIT_WINDOW = 60 # 1 minute | |
| RATE_LIMIT_MAX_REQUESTS = 10 | |
| request_history = defaultdict(list) | |
| model = None | |
| HAS_YOLO = False | |
| is_downloading = False | |
| classifier_model = None | |
| preprocess = None | |
| categories = None | |
| HAS_CLASSIFIER = False | |
| def download_ip102_model(): | |
| global is_downloading, model, HAS_YOLO | |
| if is_downloading: | |
| return | |
| is_downloading = True | |
| print("[IP102] Starting background download of IP102 model weights from Hugging Face...") | |
| retries = 5 | |
| for attempt in range(retries): | |
| try: | |
| req = urllib.request.Request(IP102_URL, headers={'User-Agent': 'Mozilla/5.0'}) | |
| temp_file = MODEL_IP102_FILE.with_suffix(".tmp") | |
| with urllib.request.urlopen(req, timeout=60) as response, open(temp_file, 'wb') as out_file: | |
| chunk_size = 1024 * 1024 | |
| bytes_downloaded = 0 | |
| while True: | |
| chunk = response.read(chunk_size) | |
| if not chunk: | |
| break | |
| out_file.write(chunk) | |
| bytes_downloaded += len(chunk) | |
| if bytes_downloaded % (5 * 1024 * 1024) == 0: | |
| print(f"[IP102] Download progress: {bytes_downloaded / (1024 * 1024):.1f} MB downloaded...") | |
| if temp_file.exists() and temp_file.stat().st_size > 30 * 1024 * 1024: | |
| if MODEL_IP102_FILE.exists(): | |
| MODEL_IP102_FILE.unlink() | |
| temp_file.rename(MODEL_IP102_FILE) | |
| print(f"[IP102] Download complete: {MODEL_IP102_FILE}") | |
| from ultralytics import YOLO | |
| model = YOLO(str(MODEL_IP102_FILE)) | |
| HAS_YOLO = True | |
| print(f"[IP102] Successfully loaded downloaded IP102 model with {len(model.names)} classes.") | |
| is_downloading = False | |
| return | |
| except Exception as e: | |
| print(f"[IP102] Error on download attempt {attempt+1}/{retries}: {e}") | |
| if temp_file.exists(): | |
| try: | |
| temp_file.unlink() | |
| except: | |
| pass | |
| time.sleep(3) | |
| print("[IP102] Failed to download IP102 model after all retries.") | |
| is_downloading = False | |
| def init_model(): | |
| global model, HAS_YOLO, classifier_model, preprocess, categories, HAS_CLASSIFIER | |
| # Initialize MobileNetV3 for out-of-domain checking | |
| try: | |
| from torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights | |
| weights = MobileNet_V3_Small_Weights.DEFAULT | |
| classifier_model = mobilenet_v3_small(weights=weights) | |
| classifier_model.eval() | |
| preprocess = weights.transforms() | |
| categories = weights.meta["categories"] | |
| HAS_CLASSIFIER = True | |
| print("Success: Loaded MobileNetV3 classifier for unrelated image detection.") | |
| except Exception as e: | |
| HAS_CLASSIFIER = False | |
| print(f"Warning: Failed to load MobileNetV3 classifier: {e}") | |
| try: | |
| from ultralytics import YOLO | |
| if MODEL_IP102_FILE.exists(): | |
| model = YOLO(str(MODEL_IP102_FILE)) | |
| HAS_YOLO = True | |
| print(f"Success: Loaded IP102 YOLO model from {MODEL_IP102_FILE} ({len(model.names)} classes)") | |
| return | |
| # Start downloading IP102 in the background since it does not exist | |
| print("IP102 Model file missing. Initiating background download...") | |
| threading.Thread(target=download_ip102_model, daemon=True).start() | |
| # Load cereal or fallback models as temporary fallback while downloading | |
| if MODEL_FILE.exists(): | |
| model = YOLO(str(MODEL_FILE)) | |
| HAS_YOLO = True | |
| print(f"Success: Loaded Cereal Pests custom YOLO model as temporary fallback from {MODEL_FILE} ({len(model.names)} classes)") | |
| return | |
| fallback_path = Path(r"C:\Users\fadhi\runs\detect\train-3\weights\best.pt") | |
| if fallback_path.exists(): | |
| model = YOLO(str(fallback_path)) | |
| HAS_YOLO = True | |
| print(f"Success: Loaded fallback YOLO model as temporary fallback from {fallback_path} ({len(model.names)} classes)") | |
| else: | |
| print("No fallback model available. Server will run in Mock Mode until download finishes.") | |
| except Exception as e: | |
| if not HAS_YOLO: | |
| HAS_YOLO = False | |
| print(f"Warning: Failed to load YOLO model: {e}. Using dummy detection.") | |
| else: | |
| print(f"Warning: Fallback model loaded, but error occurred: {e}") | |
| init_model() | |
| def normalize_key(name): | |
| return name.lower().replace("-", " ").replace("_", " ").strip() | |
| def get_ip102_category_and_treatment(cls_id, raw_name): | |
| if 0 <= cls_id <= 13: | |
| return "Rice Pest", "Rice Pest: Maintain proper water levels, avoid excess nitrogen, use light traps, and introduce natural predators like ducklings or frogs. Apply specific systemic insecticides if threshold is exceeded." | |
| elif 14 <= cls_id <= 21: | |
| return "Soil/General Pest", "Soil/Root Pest: Use crop rotation, deep tillage during winter to expose pests, and apply biological control agents like entomopathogenic nematodes or soil-applied bio-pesticides." | |
| elif 22 <= cls_id <= 23: | |
| return "Corn Pest", "Corn Pest: Rotate crops with legumes, use pheromone traps, plant pest-resistant hybrids (e.g., Bt corn), and apply targeted biological sprays when larvae are young." | |
| elif 24 <= cls_id <= 35: | |
| return "Wheat Pest", "Wheat Pest: Maintain weed-free borders, practice early planting, conserve natural insect predators (like ladybugs), and apply recommended selective foliar treatments if infestation is high." | |
| elif 36 <= cls_id <= 43: | |
| return "Beet Pest", "Beet Pest: Remove weed hosts, practice crop rotation, use row covers for young crops, and apply neem-based sprays or target selective insecticides early in the cycle." | |
| elif 44 <= cls_id <= 55: | |
| return "Alfalfa Pest", "Alfalfa Pest: Harvest early if damage is visible to disrupt life cycles, use resistant varieties, and apply selective insecticides only if the economic threshold is crossed to save beneficial insects." | |
| elif 56 <= cls_id <= 72: | |
| return "Grape Pest", "Grape/Vine Pest: Prune affected leaves, use yellow sticky traps, maintain vineyard hygiene, and apply mineral oil or targeted insecticidal soaps for sucking pests." | |
| elif 73 <= cls_id <= 91: | |
| return "Citrus Pest", "Citrus Pest: Prune infested shoots, encourage beneficial predators (e.g., predatory mites or wasps), use horticultural oil sprays, and apply targeted systemic treatments if necessary." | |
| elif 92 <= cls_id <= 101: | |
| return "Mango Pest", "Mango Pest: Maintain orchard sanitation, prune dense branches to improve sunlight penetration, use sticky bands on tree trunks, and apply specific crop protection sprays during flowering." | |
| return "Unknown Pest", "Consult with a local agricultural officer for specific treatment advice." | |
| PEST_TREATMENT = { | |
| "brown planthopper": "Avoid excessive nitrogen fertilizer. Use resistant varieties and specific insecticides like Buprofezin.", | |
| "brown plant hopper": "Avoid excessive nitrogen fertilizer. Use resistant varieties and specific insecticides like Buprofezin.", | |
| "green leafhopper": "Use light traps. Apply appropriate systemic insecticides to prevent Tungro disease transmission.", | |
| "rice leafhopper": "Use light traps. Apply appropriate systemic insecticides to prevent Tungro disease transmission.", | |
| "leaf folder": "Conserve natural enemies. Use neem-based pesticides or Cartap Hydrochloride if damage is severe.", | |
| "rice leaf roller": "Conserve natural enemies. Use neem-based pesticides or Cartap Hydrochloride if damage is severe.", | |
| "rice bug": "Maintain clean fields. Apply contact insecticides (e.g., Lambda-cyhalothrin) during early morning or late evening.", | |
| "stem borer": "Maintain proper field drainage, use pheromone traps, and apply recommended chemical treatments like Carbofuran.", | |
| "asiatic rice borer": "Maintain proper field drainage, use pheromone traps, and apply recommended chemical treatments like Carbofuran.", | |
| "yellow rice borer": "Maintain proper field drainage, use pheromone traps, and apply recommended chemical treatments like Carbofuran.", | |
| "whorl maggot": "Drain the field for a few days to reduce maggot survival. Apply appropriate foliar sprays if infestation is high.", | |
| "paddy stem maggot": "Drain the field for a few days to reduce maggot survival. Apply appropriate foliar sprays if infestation is high.", | |
| "fall army worm": "Use biological control agents (like Trichogramma wasps) or apply recommended chemical treatments like Spinetoram or Chlorantraniliprole.", | |
| "army worm": "Use biological control agents (like Trichogramma wasps) or apply recommended chemical treatments like Spinetoram or Chlorantraniliprole.", | |
| "northern corn rootworm": "Rotate crops with non-host plants (like soybeans). Use soil insecticides or crop varieties containing Bt traits.", | |
| "southern corn rootworm": "Practice early planting, rotate crops, and apply recommended soil insecticides during planting if history of damage exists.", | |
| "western corn rootworm": "Use crop rotation. Apply granular soil insecticides at planting, or plant Bt rootworm-resistant corn hybrids.", | |
| "aphids": "Use insecticidal soaps, neem oil, or introduce natural predators like ladybugs.", | |
| "none": "No treatment required. Your crops look healthy!", | |
| "default": "Consult with a local agricultural officer for specific treatment advice.", | |
| } | |
| ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"} | |
| def allowed_file(filename): | |
| return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS | |
| def index(): | |
| model_info = "Disabled" | |
| if HAS_YOLO and model: | |
| model_info = f"Enabled ({len(model.names)} classes)" | |
| return f"Pest Detection API is running! YOLO Model: {model_info}" | |
| def health(request: Request): | |
| classes_count = 0 | |
| model_type = "None" | |
| if HAS_YOLO and model: | |
| classes_count = len(model.names) | |
| model_type = "IP102 (102 pests)" if classes_count == 102 else "Cereal Pests (10 pests)" | |
| return { | |
| "status": "ok", | |
| "yolo": HAS_YOLO, | |
| "model_type": model_type, | |
| "classes_count": classes_count, | |
| "is_downloading_ip102": is_downloading | |
| } | |
| async def predict( | |
| request: Request, | |
| image: UploadFile = File(...), | |
| device_id: Optional[str] = Form(None) | |
| ): | |
| # Resolve real client IP address behind reverse proxies | |
| ip = "unknown" | |
| if request.client: | |
| ip = request.client.host | |
| forwarded_for = request.headers.get("x-forwarded-for") | |
| if forwarded_for: | |
| ip = forwarded_for.split(",")[0].strip() | |
| # Rate limiting check (excludes development environments) | |
| is_local = ip in ("localhost", "unknown") | |
| if not is_local: | |
| try: | |
| ip_obj = ipaddress.ip_address(ip) | |
| is_local = ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_link_local | |
| except ValueError: | |
| pass | |
| if not is_local: | |
| now = time.time() | |
| request_history[ip] = [t for t in request_history[ip] if now - t < RATE_LIMIT_WINDOW] | |
| if len(request_history[ip]) >= RATE_LIMIT_MAX_REQUESTS: | |
| log_audit("anonymous", "/predict", "rate-limited", f"Rate limit exceeded (IP: {ip})", ip) | |
| raise HTTPException(status_code=429, detail="Too many requests. Please try again later.") | |
| request_history[ip].append(now) | |
| if not image.filename: | |
| log_audit("anonymous", "/predict", "failed", "No selected file", ip) | |
| raise HTTPException(status_code=400, detail="No selected file") | |
| if not allowed_file(image.filename): | |
| log_audit("anonymous", "/predict", "failed", f"File type not allowed: {image.filename}", ip) | |
| raise HTTPException(status_code=400, detail="File type not allowed") | |
| try: | |
| contents = await image.read() | |
| if len(contents) > 5 * 1024 * 1024: | |
| log_audit("anonymous", "/predict", "failed", "File too large (exceeded 5MB)", ip) | |
| raise HTTPException(status_code=413, detail="File size exceeds the 5MB limit.") | |
| img = Image.open(io.BytesIO(contents)).convert("RGB") | |
| except HTTPException as he: | |
| raise he | |
| except Exception as e: | |
| log_audit("anonymous", "/predict", "error", f"Image parsing error: {str(e)}", ip) | |
| raise HTTPException(status_code=500, detail=f"Failed to process image: {str(e)}") | |
| # Check if the image is related to crops/plants/pests unconditionally | |
| if HAS_CLASSIFIER and classifier_model: | |
| try: | |
| import torch | |
| img_tensor = preprocess(img).unsqueeze(0) | |
| with torch.no_grad(): | |
| outputs = classifier_model(img_tensor) | |
| probs = torch.nn.functional.softmax(outputs[0], dim=0) | |
| top5_indices = torch.topk(probs, 5).indices.tolist() | |
| top5_names = [categories[idx] for idx in top5_indices] | |
| related_keywords = { | |
| 'leaf', 'plant', 'tree', 'flower', 'crop', 'insect', 'bug', 'spider', 'caterpillar', | |
| 'moth', 'butterfly', 'grasshopper', 'beetle', 'cricket', 'ant', 'fly', 'wasp', 'bee', | |
| 'aphid', 'weevil', 'locust', 'cicada', 'mantis', 'ladybug', 'mite', 'slug', 'snail', | |
| 'worm', 'larva', 'pupa', 'grub', 'vegetable', 'fruit', 'cereal', 'grass', 'grain', | |
| 'corn', 'maize', 'rice', 'wheat', 'barley', 'soil', 'ground', 'earth', 'dirt', | |
| 'nature', 'field', 'farm', 'garden', 'agriculture', 'pest', 'fungus', 'rust', | |
| 'rot', 'mildew', 'spot', 'blight', 'mold', 'banana', 'apple', 'orange', 'broccoli', | |
| 'cabbage', 'tomato', 'potato', 'seed', 'sprout', 'stem', 'branch', 'root', 'wood', | |
| 'bark', 'forest', 'jungle', 'meadow', 'vegetation', 'flora', 'fauna', 'herb', | |
| 'conifer', 'fern', 'moss', 'lichen', 'shrub', 'bush', 'vine', 'foliage', | |
| 'lizard', 'snake', 'chameleon', 'gecko', 'walking stick', 'frog', 'toad', 'salamander', | |
| 'iguana', 'anole', 'dragon' | |
| } | |
| is_related = False | |
| for name in top5_names: | |
| name_lower = name.lower() | |
| if any(kw in name_lower for kw in related_keywords): | |
| is_related = True | |
| break | |
| if not is_related: | |
| detected_obj = top5_names[0].replace("_", " ").title() | |
| log_details = json.dumps({"pest": "Invalid Image", "detected": detected_obj, "filename": image.filename}) | |
| log_audit("anonymous", "/predict", "invalid-image", log_details, ip) | |
| return { | |
| "status": "success", | |
| "pest_detected": "Invalid Image", | |
| "confidence": 0.0, | |
| "treatment": "Please upload a clear image of a crop leaf or pest.", | |
| "message": f"This does not look like a crop or pest image (detected: {detected_obj}).", | |
| } | |
| except Exception as ex: | |
| print(f"Error running unrelated image validation: {ex}") | |
| if HAS_YOLO: | |
| try: | |
| # Run with a lower confidence threshold of 0.20 to identify potential unrecognized pests | |
| results = model.predict(source=img, save=False, conf=0.20) | |
| detections = [] | |
| for r in results: | |
| for box in r.boxes: | |
| cls_id = int(box.cls[0]) | |
| raw_name = model.names[cls_id] | |
| formatted_name = raw_name.replace("-", " ").title() | |
| conf = float(box.conf[0]) | |
| norm_name = normalize_key(raw_name) | |
| if norm_name in PEST_TREATMENT: | |
| treatment = PEST_TREATMENT[norm_name] | |
| elif len(model.names) == 102: | |
| _, treatment = get_ip102_category_and_treatment(cls_id, raw_name) | |
| else: | |
| treatment = PEST_TREATMENT["default"] | |
| detections.append({ | |
| "pest_detected": formatted_name, | |
| "confidence": round(conf, 2), | |
| "treatment": treatment, | |
| }) | |
| # Sort by confidence descending | |
| detections.sort(key=lambda x: x["confidence"], reverse=True) | |
| if not detections: | |
| log_details = json.dumps({"pest": "None", "confidence": 0.0, "filename": image.filename}) | |
| log_audit("anonymous", "/predict", "success", log_details, ip) | |
| return { | |
| "status": "success", | |
| "pest_detected": "None", | |
| "confidence": 0.0, | |
| "treatment": PEST_TREATMENT["none"], | |
| "message": "No pests detected", | |
| } | |
| best_det = detections[0] | |
| best_conf = best_det["confidence"] | |
| if best_conf >= 0.40: | |
| # Normal high confidence detection | |
| log_details = json.dumps({ | |
| "pest": best_det["pest_detected"], | |
| "confidence": best_det["confidence"], | |
| "filename": image.filename | |
| }) | |
| log_audit("anonymous", "/predict", "success", log_details, ip) | |
| # Filter detections >= 0.40 for backward compatibility | |
| filtered_detections = [ | |
| { | |
| "pest_detected": d["pest_detected"], | |
| "confidence": d["confidence"], | |
| "treatment": d["treatment"] | |
| } | |
| for d in detections if d["confidence"] >= 0.40 | |
| ] | |
| return { | |
| "status": "success", | |
| "pest_detected": best_det["pest_detected"], | |
| "confidence": best_det["confidence"], | |
| "treatment": best_det["treatment"], | |
| "message": f"Detected {len(filtered_detections)} pests", | |
| "all_detections": filtered_detections, | |
| } | |
| else: | |
| # Low confidence (0.20 <= best_conf < 0.40) -> Auto-report as Unrecognized Pest | |
| dev_id = device_id if device_id else "unknown_device" | |
| # Save the image to the unrecognized folder | |
| unrecognized_dir = BASE_DIR / "uploads" / "unrecognized" | |
| unrecognized_dir.mkdir(parents=True, exist_ok=True) | |
| ext = image.filename.rsplit(".", 1)[1].lower() if "." in image.filename else "jpg" | |
| filename = f"{dev_id}_{int(time.time())}.{ext}" | |
| file_path = unrecognized_dir / filename | |
| with open(file_path, "wb") as f: | |
| f.write(contents) | |
| # Insert into DB | |
| report_id = None | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "INSERT INTO unrecognized_reports (device_id, image_path) VALUES (?, ?)", | |
| (dev_id, filename) | |
| ) | |
| report_id = cursor.lastrowid | |
| conn.commit() | |
| conn.close() | |
| except Exception as db_ex: | |
| print(f"Error saving auto-reported unrecognized pest: {db_ex}") | |
| log_details = json.dumps({ | |
| "pest": "Pest Not Recognized", | |
| "confidence": best_conf, | |
| "filename": image.filename, | |
| "report_id": report_id | |
| }) | |
| log_audit("anonymous", "/predict", "unrecognized-pest", log_details, ip) | |
| return { | |
| "status": "success", | |
| "pest_detected": "Pest Not Recognized", | |
| "confidence": best_conf, | |
| "treatment": "We couldn't identify this pest right now. Our experts are looking into it. Please wait for a while; we will notify you as soon as the results are available.", | |
| "report_id": report_id, | |
| "message": "Pest detected with low confidence, auto-reported for admin resolution." | |
| } | |
| except Exception as e: | |
| log_audit("anonymous", "/predict", "error", f"YOLO Inference error: {str(e)}", ip) | |
| raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}") | |
| else: | |
| # Fallback Mock Mode logic | |
| log_details = json.dumps({"pest": "Aphids (Mock)", "confidence": 0.95, "filename": image.filename}) | |
| log_audit("anonymous", "/predict", "success", log_details, ip) | |
| return { | |
| "status": "success", | |
| "pest_detected": "Aphids (Mock)", | |
| "confidence": 0.95, | |
| "treatment": PEST_TREATMENT["aphids"], | |
| "message": "Detection completed successfully (Mock Mode)", | |
| } | |
| async def report_unrecognized( | |
| request: Request, | |
| device_id: str = Form(...), | |
| image: UploadFile = File(...) | |
| ): | |
| ip = "unknown" | |
| if request.client: | |
| ip = request.client.host | |
| forwarded_for = request.headers.get("x-forwarded-for") | |
| if forwarded_for: | |
| ip = forwarded_for.split(",")[0].strip() | |
| if not image.filename or not allowed_file(image.filename): | |
| log_audit("anonymous", "/predict/report-unrecognized", "failed", "Invalid file type", ip) | |
| raise HTTPException(status_code=400, detail="Invalid file type") | |
| try: | |
| contents = await image.read() | |
| if len(contents) > 10 * 1024 * 1024: | |
| log_audit("anonymous", "/predict/report-unrecognized", "failed", "File too large", ip) | |
| raise HTTPException(status_code=413, detail="File size exceeds the 10MB limit.") | |
| unrecognized_dir = BASE_DIR / "uploads" / "unrecognized" | |
| unrecognized_dir.mkdir(parents=True, exist_ok=True) | |
| ext = image.filename.rsplit(".", 1)[1].lower() | |
| filename = f"{device_id}_{int(time.time())}.{ext}" | |
| file_path = unrecognized_dir / filename | |
| with open(file_path, "wb") as f: | |
| f.write(contents) | |
| except HTTPException as he: | |
| raise he | |
| except Exception as e: | |
| log_audit("anonymous", "/predict/report-unrecognized", "error", f"Save error: {e}", ip) | |
| raise HTTPException(status_code=500, detail=f"Failed to save image: {e}") | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "INSERT INTO unrecognized_reports (device_id, image_path) VALUES (?, ?)", | |
| (device_id, filename) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| log_audit("anonymous", "/predict/report-unrecognized", "success", f"Report created: {filename}", ip) | |
| return {"status": "success", "message": "Report submitted successfully"} | |
| except Exception as e: | |
| log_audit("anonymous", "/predict/report-unrecognized", "error", f"Database error: {e}", ip) | |
| raise HTTPException(status_code=500, detail=f"Database error: {e}") | |
| def get_reports_status(device_id: str): | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| conn.row_factory = sqlite3.Row | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "SELECT id, image_path, status, pest_name, treatment, created_at FROM unrecognized_reports WHERE device_id = ? ORDER BY created_at DESC", | |
| (device_id,) | |
| ) | |
| rows = cursor.fetchall() | |
| conn.close() | |
| reports = [] | |
| for r in rows: | |
| reports.append({ | |
| "id": r["id"], | |
| "image_path": r["image_path"], | |
| "status": r["status"], | |
| "pest_name": r["pest_name"] if r["pest_name"] else "", | |
| "treatment": r["treatment"] if r["treatment"] else "", | |
| "created_at": r["created_at"] | |
| }) | |
| return {"status": "success", "reports": reports} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {e}") | |
| def admin_dashboard(): | |
| admin_html_file = BASE_DIR / "admin.html" | |
| if admin_html_file.exists(): | |
| with open(admin_html_file, "r", encoding="utf-8") as f: | |
| return HTMLResponse(content=f.read()) | |
| return HTMLResponse(content="<h1>Admin Dashboard Template Missing</h1>") | |
| def admin_get_reports(authenticated: bool = Depends(authenticate_admin)): | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| conn.row_factory = sqlite3.Row | |
| cursor = conn.cursor() | |
| cursor.execute("SELECT id, device_id, image_path, status, pest_name, treatment, created_at FROM unrecognized_reports ORDER BY created_at DESC") | |
| rows = cursor.fetchall() | |
| conn.close() | |
| reports = [] | |
| for r in rows: | |
| reports.append({ | |
| "id": r["id"], | |
| "device_id": r["device_id"], | |
| "image_path": r["image_path"], | |
| "status": r["status"], | |
| "pest_name": r["pest_name"] if r["pest_name"] else "", | |
| "treatment": r["treatment"] if r["treatment"] else "", | |
| "created_at": r["created_at"] | |
| }) | |
| return {"status": "success", "reports": reports} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {e}") | |
| def admin_resolve_report( | |
| report_id: int, | |
| pest_name: str = Form(...), | |
| treatment: str = Form(...), | |
| authenticated: bool = Depends(authenticate_admin) | |
| ): | |
| try: | |
| conn = sqlite3.connect(str(DB_FILE), timeout=30.0) | |
| cursor = conn.cursor() | |
| cursor.execute( | |
| "UPDATE unrecognized_reports SET status = 'resolved', pest_name = ?, treatment = ? WHERE id = ?", | |
| (pest_name, treatment, report_id) | |
| ) | |
| conn.commit() | |
| conn.close() | |
| return {"status": "success", "message": f"Report {report_id} resolved successfully"} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {e}") | |
| def admin_serve_image(filename: str, authenticated: bool = Depends(authenticate_admin)): | |
| file_path = BASE_DIR / "uploads" / "unrecognized" / filename | |
| if file_path.exists(): | |
| return FileResponse(str(file_path)) | |
| raise HTTPException(status_code=404, detail="Image not found") | |
| if __name__ == "__main__": | |
| import uvicorn | |
| port = int(os.environ.get("PORT", 5000)) | |
| uvicorn.run(app, host="0.0.0.0", port=port) | |