Your Name commited on
Commit
5354624
Β·
1 Parent(s): 441e215

Deploy Crop Classifier API v2

Browse files
.env.example ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copy this file to .env and fill in your values
2
+ # Never commit your real .env to GitHub!
3
+
4
+ # Comma-separated list of API keys in format: key:label
5
+ # Example:
6
+ API_KEYS=your-secret-key-here:KisanSetuApp,another-key-456:ExternalUser
7
+
8
+ # Paths to model files (defaults to current directory)
9
+ MODEL_PATH=best_v6.keras
10
+ JSON_PATH=class_names.json
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ *.keras filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Python slim image
2
+ FROM python:3.11-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies for TensorFlow
8
+ RUN apt-get update && apt-get install -y \
9
+ libhdf5-dev \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Copy requirements first (for Docker cache efficiency)
13
+ COPY requirements.txt .
14
+ RUN pip install --no-cache-dir -r requirements.txt
15
+
16
+ # Copy model files and API code
17
+ COPY best_v6.keras .
18
+ COPY class_names.json .
19
+ COPY main.py .
20
+
21
+ # Expose port
22
+ EXPOSE 7860
23
+
24
+ # Start the FastAPI server
25
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,12 +1,19 @@
1
  ---
2
- title: Crop Classifier Api
3
- emoji: πŸ‘€
4
- colorFrom: red
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
- license: apache-2.0
9
- short_description: predict crop , its quality, and further info.
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Crop Classifier API
3
+ emoji: 🌾
4
+ colorFrom: green
5
  colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
+ app_port: 7860
 
9
  ---
10
 
11
+ # 🌾 Crop Classifier API
12
+
13
+ AI-powered crop classification API built on **EfficientNetB3 v6** (93.48% accuracy) + **LLaMA-3.2-90B Vision** expert verification.
14
+
15
+ ## Usage
16
+
17
+ `POST /predict` with your image + `x-api-key` header.
18
+
19
+ See `/docs` for interactive Swagger UI.
__pycache__/main.cpython-313.pyc ADDED
Binary file (20.5 kB). View file
 
best_v6.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed5ff4a253b2396abb9d5ec10a3c8fea4c42d05409d7c082b16f4ac28f5813f3
3
+ size 161426146
class_names.json ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "class_names": [
3
+ "Almonds plant",
4
+ "Apples plant",
5
+ "Avocados plant",
6
+ "Bananas plant",
7
+ "Barley plant",
8
+ "Broccoli plant",
9
+ "Cabbages and other brassicas plant",
10
+ "Carrots and turnips plant",
11
+ "Cashew nuts plant",
12
+ "Cassava plant",
13
+ "Cauliflower plant",
14
+ "Chickpeas plant",
15
+ "Chili peppers and green peppers plant",
16
+ "Coconuts plant",
17
+ "Coffee (green) plant",
18
+ "Cotton lint plant",
19
+ "Cucumbers and gherkins plant",
20
+ "Dates plant",
21
+ "Eggplants (Aubergines) plant",
22
+ "Garlic plant",
23
+ "Ginger plant",
24
+ "Grapes plant",
25
+ "Groundnuts (Peanuts) plant",
26
+ "Jackfruit plant",
27
+ "Lemons and limes plant",
28
+ "Lentils plant",
29
+ "Maize (Corn) plant",
30
+ "Mangoes mangosteens guavas plant",
31
+ "Millet plant",
32
+ "Mustard seeds plant",
33
+ "Oats plant",
34
+ "Onions (dry) plant",
35
+ "Oranges plant",
36
+ "Papayas plant",
37
+ "Peas (Green) plant",
38
+ "Pineapples plant",
39
+ "Potatoes plant",
40
+ "Rice (Paddy) plant",
41
+ "Sorghum plant",
42
+ "Soybeans plant",
43
+ "Spinach plant",
44
+ "Strawberries plant",
45
+ "Sugar cane plant",
46
+ "Sunflower seeds plant",
47
+ "Sweet potatoes plant",
48
+ "Tea plant",
49
+ "Tomatoes plant",
50
+ "Turmeric plant",
51
+ "Watermelons plant",
52
+ "Wheat plant"
53
+ ],
54
+ "class_indices": {
55
+ "Almonds plant": 0,
56
+ "Apples plant": 1,
57
+ "Avocados plant": 2,
58
+ "Bananas plant": 3,
59
+ "Barley plant": 4,
60
+ "Broccoli plant": 5,
61
+ "Cabbages and other brassicas plant": 6,
62
+ "Carrots and turnips plant": 7,
63
+ "Cashew nuts plant": 8,
64
+ "Cassava plant": 9,
65
+ "Cauliflower plant": 10,
66
+ "Chickpeas plant": 11,
67
+ "Chili peppers and green peppers plant": 12,
68
+ "Coconuts plant": 13,
69
+ "Coffee (green) plant": 14,
70
+ "Cotton lint plant": 15,
71
+ "Cucumbers and gherkins plant": 16,
72
+ "Dates plant": 17,
73
+ "Eggplants (Aubergines) plant": 18,
74
+ "Garlic plant": 19,
75
+ "Ginger plant": 20,
76
+ "Grapes plant": 21,
77
+ "Groundnuts (Peanuts) plant": 22,
78
+ "Jackfruit plant": 23,
79
+ "Lemons and limes plant": 24,
80
+ "Lentils plant": 25,
81
+ "Maize (Corn) plant": 26,
82
+ "Mangoes mangosteens guavas plant": 27,
83
+ "Millet plant": 28,
84
+ "Mustard seeds plant": 29,
85
+ "Oats plant": 30,
86
+ "Onions (dry) plant": 31,
87
+ "Oranges plant": 32,
88
+ "Papayas plant": 33,
89
+ "Peas (Green) plant": 34,
90
+ "Pineapples plant": 35,
91
+ "Potatoes plant": 36,
92
+ "Rice (Paddy) plant": 37,
93
+ "Sorghum plant": 38,
94
+ "Soybeans plant": 39,
95
+ "Spinach plant": 40,
96
+ "Strawberries plant": 41,
97
+ "Sugar cane plant": 42,
98
+ "Sunflower seeds plant": 43,
99
+ "Sweet potatoes plant": 44,
100
+ "Tea plant": 45,
101
+ "Tomatoes plant": 46,
102
+ "Turmeric plant": 47,
103
+ "Watermelons plant": 48,
104
+ "Wheat plant": 49
105
+ },
106
+ "idx_to_class": {
107
+ "0": "Almonds plant",
108
+ "1": "Apples plant",
109
+ "2": "Avocados plant",
110
+ "3": "Bananas plant",
111
+ "4": "Barley plant",
112
+ "5": "Broccoli plant",
113
+ "6": "Cabbages and other brassicas plant",
114
+ "7": "Carrots and turnips plant",
115
+ "8": "Cashew nuts plant",
116
+ "9": "Cassava plant",
117
+ "10": "Cauliflower plant",
118
+ "11": "Chickpeas plant",
119
+ "12": "Chili peppers and green peppers plant",
120
+ "13": "Coconuts plant",
121
+ "14": "Coffee (green) plant",
122
+ "15": "Cotton lint plant",
123
+ "16": "Cucumbers and gherkins plant",
124
+ "17": "Dates plant",
125
+ "18": "Eggplants (Aubergines) plant",
126
+ "19": "Garlic plant",
127
+ "20": "Ginger plant",
128
+ "21": "Grapes plant",
129
+ "22": "Groundnuts (Peanuts) plant",
130
+ "23": "Jackfruit plant",
131
+ "24": "Lemons and limes plant",
132
+ "25": "Lentils plant",
133
+ "26": "Maize (Corn) plant",
134
+ "27": "Mangoes mangosteens guavas plant",
135
+ "28": "Millet plant",
136
+ "29": "Mustard seeds plant",
137
+ "30": "Oats plant",
138
+ "31": "Onions (dry) plant",
139
+ "32": "Oranges plant",
140
+ "33": "Papayas plant",
141
+ "34": "Peas (Green) plant",
142
+ "35": "Pineapples plant",
143
+ "36": "Potatoes plant",
144
+ "37": "Rice (Paddy) plant",
145
+ "38": "Sorghum plant",
146
+ "39": "Soybeans plant",
147
+ "40": "Spinach plant",
148
+ "41": "Strawberries plant",
149
+ "42": "Sugar cane plant",
150
+ "43": "Sunflower seeds plant",
151
+ "44": "Sweet potatoes plant",
152
+ "45": "Tea plant",
153
+ "46": "Tomatoes plant",
154
+ "47": "Turmeric plant",
155
+ "48": "Watermelons plant",
156
+ "49": "Wheat plant"
157
+ },
158
+ "num_classes": 50,
159
+ "img_size": 224
160
+ }
main.py ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Crop Classifier REST API v2.0
3
+ ===============================
4
+ FastAPI server wrapping the EfficientNetB3 crop classification model (v6).
5
+ Supports 50 crop varieties with API key authentication.
6
+
7
+ Improvements in v2:
8
+ - Richer LLaMA output: scientific name, market grade, storage tip, prediction accuracy
9
+ - Confidence labels (High / Medium / Low) on every prediction
10
+ - Combined final_verdict field
11
+ - Request ID + timestamp on every response
12
+ - /predict/fast endpoint (model only, no LLaMA) for speed-sensitive callers
13
+ """
14
+
15
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Header, Depends, Query
16
+ from fastapi.middleware.cors import CORSMiddleware
17
+ import numpy as np
18
+ import json
19
+ import os
20
+ from PIL import Image
21
+ import tensorflow as tf
22
+ from tensorflow.keras.applications.efficientnet import preprocess_input
23
+ import io
24
+ import time
25
+ import logging
26
+ import base64
27
+ import requests
28
+ import uuid
29
+ from datetime import datetime, timezone
30
+
31
+ # ─────────────────────────────────────────────
32
+ # Logging
33
+ # ─────────────────────────────────────────────
34
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
35
+ logger = logging.getLogger(__name__)
36
+
37
+ # ─────────────────────────────────────────────
38
+ # App
39
+ # ─────────────────────────────────────────────
40
+ app = FastAPI(
41
+ title="🌾 Crop Classifier API",
42
+ description=(
43
+ "AI-powered REST API to classify crop images into 50 varieties.\n\n"
44
+ "**Model:** EfficientNetB3 v6 (93.48% accuracy)\n"
45
+ "**AI Expert:** LLaMA-3.2-90B Vision (NVIDIA)\n\n"
46
+ "### How to use\n"
47
+ "1. Get an API key from the admin.\n"
48
+ "2. `POST /predict` with your image + `x-api-key` header.\n"
49
+ "3. Get structured JSON with crop name, quality, grade, storage tips.\n\n"
50
+ "### Endpoints\n"
51
+ "- `POST /predict` β€” Full analysis (model + LLaMA expert)\n"
52
+ "- `POST /predict/fast` β€” Model only (no LLaMA, instant response)\n"
53
+ "- `GET /crops` β€” List all 50 supported crops\n"
54
+ ),
55
+ version="2.0.0",
56
+ )
57
+
58
+ app.add_middleware(
59
+ CORSMiddleware,
60
+ allow_origins=["*"],
61
+ allow_credentials=True,
62
+ allow_methods=["*"],
63
+ allow_headers=["*"],
64
+ )
65
+
66
+ # ─────────────────────────────────────────────
67
+ # API Keys
68
+ # ─────────────────────────────────────────────
69
+ def load_api_keys() -> dict:
70
+ raw = os.environ.get("API_KEYS", "")
71
+ keys = {}
72
+ if raw:
73
+ for entry in raw.split(","):
74
+ parts = entry.strip().split(":", 1)
75
+ if len(parts) == 2:
76
+ keys[parts[0]] = parts[1]
77
+ if not keys:
78
+ keys = {
79
+ "dev-test-key-12345": "Local Development",
80
+ "kisansetu-app-key-99": "KisanSetu WebApp",
81
+ }
82
+ return keys
83
+
84
+ VALID_API_KEYS: dict = load_api_keys()
85
+
86
+
87
+ def validate_api_key(x_api_key: str = Header(..., description="Your API key")):
88
+ if x_api_key not in VALID_API_KEYS:
89
+ logger.warning(f"Rejected invalid API key: {x_api_key[:8]}...")
90
+ raise HTTPException(status_code=401, detail={
91
+ "error": "Unauthorized",
92
+ "message": "Invalid or missing API key.",
93
+ })
94
+ return VALID_API_KEYS[x_api_key]
95
+
96
+ # ─────────────────────────────────────────────
97
+ # Model Loading
98
+ # ─────────────────────────────────────────────
99
+ MODEL_PATH = os.environ.get("MODEL_PATH", "best_v6.keras")
100
+ JSON_PATH = os.environ.get("JSON_PATH", "class_names.json")
101
+
102
+ logger.info(f"Loading model: {MODEL_PATH}")
103
+ model = tf.keras.models.load_model(MODEL_PATH)
104
+ logger.info("Model loaded.")
105
+
106
+ with open(JSON_PATH) as f:
107
+ class_names: list = json.load(f)["class_names"]
108
+ logger.info(f"Loaded {len(class_names)} classes.")
109
+
110
+ # ─────────────────────────────────────────────
111
+ # Helpers
112
+ # ─────────────────────────────────────────────
113
+ NVIDIA_API_KEY = os.environ.get(
114
+ "NVIDIA_API_KEY",
115
+ "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"
116
+ )
117
+ LLAMA_URL = "https://integrate.api.nvidia.com/v1/chat/completions"
118
+
119
+
120
+ def confidence_label(pct: float) -> str:
121
+ """Convert confidence % to human-readable label."""
122
+ if pct >= 70: return "High"
123
+ if pct >= 40: return "Medium"
124
+ return "Low"
125
+
126
+
127
+ def preprocess_image(image_bytes: bytes) -> np.ndarray:
128
+ try:
129
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
130
+ except Exception:
131
+ raise HTTPException(status_code=422, detail="Cannot decode image. Upload a valid JPG/PNG/WEBP/BMP file.")
132
+ image = image.resize((224, 224))
133
+ arr = np.expand_dims(np.array(image, dtype=np.float32), axis=0)
134
+ return preprocess_input(arr)
135
+
136
+
137
+ def compress_image(image_bytes: bytes) -> bytes:
138
+ """Resize to 768Γ—768 JPEG-85 for LLaMA β€” balanced quality vs payload size."""
139
+ try:
140
+ img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
141
+ img.thumbnail((768, 768))
142
+ buf = io.BytesIO()
143
+ img.save(buf, format="JPEG", quality=85, optimize=True)
144
+ return buf.getvalue()
145
+ except Exception:
146
+ return image_bytes
147
+
148
+
149
+ def call_llama_vision(image_bytes: bytes, top3_preds: list) -> dict:
150
+ """
151
+ Call NVIDIA LLaMA-3.2-90B Vision for expert crop analysis.
152
+ Returns structured fields + richer agronomic data.
153
+ """
154
+ try:
155
+ compressed = compress_image(image_bytes)
156
+ img_b64 = base64.b64encode(compressed).decode("utf-8")
157
+
158
+ predictions_str = ", ".join(
159
+ f"{p['crop']} ({p['confidence_percent']}%)" for p in top3_preds
160
+ )
161
+
162
+ prompt = (
163
+ "You are an expert agricultural scientist and crop quality inspector with 20 years of experience.\n"
164
+ "Carefully analyze the crop or agricultural product shown in this image.\n\n"
165
+ f"An automated vision model suggests it might be: {predictions_str}\n\n"
166
+ "Respond ONLY in this exact format β€” no extra text, no preamble:\n\n"
167
+ "**Crop Name:** [Correct common name of the crop/product]\n"
168
+ "**Scientific Name:** [Latin/scientific name, or 'N/A' if unknown]\n"
169
+ "**Characteristics:** [Visual features: color, shape, texture, size, form]\n"
170
+ "**Quality:** [Choose ONE: Premium, Excellent, Very Good, Good, Fair, or Bad]\n"
171
+ "**Market Grade:** [Choose ONE: Grade A, Grade B, Grade C, or Ungraded]\n"
172
+ "**Prediction Accuracy:** [Is the model correct? Choose ONE: Correct, Partially Correct, or Incorrect]\n"
173
+ "**Storage Tip:** [One practical tip for storing or handling this crop]\n"
174
+ "**Explanation:** [2-3 sentences explaining your identification and quality assessment]"
175
+ )
176
+
177
+ payload = {
178
+ "model": "meta/llama-3.2-90b-vision-instruct",
179
+ "messages": [{"role": "user", "content": [
180
+ {"type": "text", "text": prompt},
181
+ {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"}}
182
+ ]}],
183
+ "max_tokens": 600,
184
+ "temperature": 0.3, # lower = more consistent, structured output
185
+ "top_p": 0.9,
186
+ "stream": False
187
+ }
188
+
189
+ headers = {"Authorization": f"Bearer {NVIDIA_API_KEY}", "Accept": "application/json"}
190
+ resp = requests.post(LLAMA_URL, headers=headers, json=payload, timeout=60)
191
+ resp.raise_for_status()
192
+ raw_text = resp.json()["choices"][0]["message"]["content"]
193
+
194
+ # Robust parser β€” strips leading bullets and ** markers before matching
195
+ fields = {
196
+ "crop_name": None,
197
+ "scientific_name": None,
198
+ "characteristics": None,
199
+ "quality": None,
200
+ "market_grade": None,
201
+ "prediction_accuracy": None,
202
+ "storage_tip": None,
203
+ "explanation": None,
204
+ }
205
+
206
+ for line in raw_text.splitlines():
207
+ line = line.strip()
208
+ if line.startswith("- ") or line.startswith("* "):
209
+ line = line[2:]
210
+ clean = line.replace("**", "").strip()
211
+ cl = clean.lower()
212
+
213
+ if cl.startswith("crop name:"):
214
+ fields["crop_name"] = clean.split(":", 1)[1].strip()
215
+ elif cl.startswith("scientific name:"):
216
+ fields["scientific_name"] = clean.split(":", 1)[1].strip()
217
+ elif cl.startswith("characteristics:"):
218
+ fields["characteristics"] = clean.split(":", 1)[1].strip()
219
+ elif cl.startswith("quality:"):
220
+ fields["quality"] = clean.split(":", 1)[1].strip()
221
+ elif cl.startswith("market grade:"):
222
+ fields["market_grade"] = clean.split(":", 1)[1].strip()
223
+ elif cl.startswith("prediction accuracy:"):
224
+ fields["prediction_accuracy"] = clean.split(":", 1)[1].strip()
225
+ elif cl.startswith("storage tip:"):
226
+ fields["storage_tip"] = clean.split(":", 1)[1].strip()
227
+ elif cl.startswith("explanation:"):
228
+ fields["explanation"] = clean.split(":", 1)[1].strip()
229
+
230
+ fields["raw"] = raw_text
231
+ return fields
232
+
233
+ except Exception as e:
234
+ logger.warning(f"LLaMA call failed: {e}")
235
+ return {"error": str(e), "raw": None}
236
+
237
+
238
+ def build_final_answer(top3: list, ai: dict | None) -> dict:
239
+ """
240
+ The clean, user-facing final answer β€” crop name, quality, characteristics, explanation.
241
+ Sourced from LLaMA when available, falls back to model prediction.
242
+ """
243
+ ai_ok = ai and ai.get("crop_name") and not ai.get("error")
244
+ return {
245
+ "crop_name": ai.get("crop_name") if ai_ok else top3[0]["crop"],
246
+ "quality": ai.get("quality") if ai_ok else "Unavailable",
247
+ "market_grade": ai.get("market_grade") if ai_ok else "Unavailable",
248
+ "characteristics": ai.get("characteristics") if ai_ok else "Unavailable",
249
+ "explanation": ai.get("explanation") if ai_ok else "Unavailable",
250
+ "storage_tip": ai.get("storage_tip") if ai_ok else "Unavailable",
251
+ "confidence_label":confidence_label(top3[0]["confidence_percent"]),
252
+ }
253
+
254
+ # ─────────────────────────────────────────────
255
+ # Routes
256
+ # ─────────────────────────────────────────────
257
+
258
+ @app.get("/", tags=["Info"])
259
+ def root():
260
+ return {
261
+ "api": "Crop Classifier API",
262
+ "version": "2.0.0",
263
+ "model": "EfficientNetB3 v6",
264
+ "accuracy": "93.48%",
265
+ "supported_crops": len(class_names),
266
+ "status": "online",
267
+ "endpoints": {
268
+ "full_analysis": "POST /predict",
269
+ "fast_predict": "POST /predict/fast",
270
+ "crop_list": "GET /crops",
271
+ "docs": "/docs",
272
+ }
273
+ }
274
+
275
+
276
+ @app.get("/health", tags=["Info"])
277
+ def health():
278
+ return {"status": "ok"}
279
+
280
+
281
+ @app.get("/crops", tags=["Info"])
282
+ def list_crops():
283
+ return {
284
+ "total": len(class_names),
285
+ "crops": [n.replace("_", " ").title() for n in class_names]
286
+ }
287
+
288
+
289
+ @app.post("/predict", tags=["Prediction"])
290
+ async def predict(
291
+ file: UploadFile = File(..., description="Crop image (JPG/PNG/WEBP/BMP, max 10MB)"),
292
+ client_name: str = Depends(validate_api_key),
293
+ ):
294
+ """
295
+ ## Full Crop Analysis
296
+
297
+ Runs the EfficientNetB3 model **+** LLaMA Vision expert verification.
298
+
299
+ Returns:
300
+ - Top-3 model predictions with confidence labels
301
+ - AI expert: crop name, scientific name, quality, market grade, storage tip
302
+ - Final combined verdict
303
+ - Request ID + timestamp for traceability
304
+ """
305
+ request_id = str(uuid.uuid4())
306
+ ts = datetime.now(timezone.utc).isoformat()
307
+
308
+ # Validate
309
+ allowed = {"image/jpeg", "image/png", "image/webp", "image/bmp", "image/jpg"}
310
+ if file.content_type and file.content_type not in allowed:
311
+ raise HTTPException(status_code=415, detail=f"Unsupported type: {file.content_type}. Use JPG/PNG/WEBP/BMP.")
312
+
313
+ image_bytes = await file.read()
314
+ if not image_bytes:
315
+ raise HTTPException(status_code=422, detail="File is empty.")
316
+ if len(image_bytes) > 10 * 1024 * 1024:
317
+ raise HTTPException(status_code=413, detail="File too large. Max 10MB.")
318
+
319
+ # Model inference
320
+ t0 = time.time()
321
+ preds = model.predict(preprocess_image(image_bytes), verbose=0)[0]
322
+ inference_ms = round((time.time() - t0) * 1000, 1)
323
+
324
+ top3_idx = np.argsort(preds)[-3:][::-1]
325
+ top3 = [
326
+ {
327
+ "rank": i + 1,
328
+ "crop": class_names[idx].replace("_", " ").title(),
329
+ "confidence_percent": round(float(preds[idx]) * 100, 2),
330
+ "confidence_label": confidence_label(round(float(preds[idx]) * 100, 2)),
331
+ }
332
+ for i, idx in enumerate(top3_idx)
333
+ ]
334
+
335
+ logger.info(f"[{request_id[:8]}] [{client_name}] Model β†’ {top3[0]['crop']} ({top3[0]['confidence_percent']}%) in {inference_ms}ms")
336
+
337
+ # LLaMA expert
338
+ t1 = time.time()
339
+ logger.info(f"[{request_id[:8]}] Calling LLaMA Vision...")
340
+ ai = call_llama_vision(image_bytes, top3)
341
+ llama_ms = round((time.time() - t1) * 1000, 1)
342
+ logger.info(f"[{request_id[:8]}] LLaMA done in {llama_ms}ms")
343
+
344
+ return {
345
+ "success": True,
346
+ "request_id": request_id,
347
+ "timestamp": ts,
348
+
349
+ # ── Final Answer (user-facing, all you need) ────────
350
+ "final_answer": build_final_answer(top3, ai),
351
+
352
+ # ── Model prediction ────────────────────────────────
353
+ "model_prediction": {
354
+ "top_prediction": top3[0]["crop"],
355
+ "confidence_percent": top3[0]["confidence_percent"],
356
+ "confidence_label": top3[0]["confidence_label"],
357
+ "top3": top3,
358
+ "inference_time_ms": inference_ms,
359
+ },
360
+
361
+ # ── LLaMA expert analysis ───────────────────────────
362
+ "ai_expert_verification": {
363
+ "crop_name": ai.get("crop_name"),
364
+ "scientific_name": ai.get("scientific_name"),
365
+ "characteristics": ai.get("characteristics"),
366
+ "quality": ai.get("quality"),
367
+ "market_grade": ai.get("market_grade"),
368
+ "prediction_accuracy":ai.get("prediction_accuracy"),
369
+ "storage_tip": ai.get("storage_tip"),
370
+ "explanation": ai.get("explanation"),
371
+ "llama_time_ms": llama_ms,
372
+ "raw": ai.get("raw"),
373
+ },
374
+
375
+ "model_version": "v6",
376
+ "request_by": client_name,
377
+ }
378
+
379
+
380
+ @app.post("/predict/fast", tags=["Prediction"])
381
+ async def predict_fast(
382
+ file: UploadFile = File(..., description="Crop image (JPG/PNG/WEBP/BMP, max 10MB)"),
383
+ client_name: str = Depends(validate_api_key),
384
+ ):
385
+ """
386
+ ## Fast Crop Prediction (Model Only)
387
+
388
+ Runs **only** the EfficientNetB3 model β€” no LLaMA call.
389
+ Returns results in under 500ms. Use this when speed matters more than expert verification.
390
+ """
391
+ request_id = str(uuid.uuid4())
392
+ ts = datetime.now(timezone.utc).isoformat()
393
+
394
+ image_bytes = await file.read()
395
+ if not image_bytes:
396
+ raise HTTPException(status_code=422, detail="File is empty.")
397
+ if len(image_bytes) > 10 * 1024 * 1024:
398
+ raise HTTPException(status_code=413, detail="File too large. Max 10MB.")
399
+
400
+ t0 = time.time()
401
+ preds = model.predict(preprocess_image(image_bytes), verbose=0)[0]
402
+ inference_ms = round((time.time() - t0) * 1000, 1)
403
+
404
+ top3_idx = np.argsort(preds)[-3:][::-1]
405
+ top3 = [
406
+ {
407
+ "rank": i + 1,
408
+ "crop": class_names[idx].replace("_", " ").title(),
409
+ "confidence_percent": round(float(preds[idx]) * 100, 2),
410
+ "confidence_label": confidence_label(round(float(preds[idx]) * 100, 2)),
411
+ }
412
+ for i, idx in enumerate(top3_idx)
413
+ ]
414
+
415
+ logger.info(f"[{request_id[:8]}] [FAST] [{client_name}] β†’ {top3[0]['crop']} ({top3[0]['confidence_percent']}%) in {inference_ms}ms")
416
+
417
+ return {
418
+ "success": True,
419
+ "request_id": request_id,
420
+ "timestamp": ts,
421
+ "mode": "fast (model only)",
422
+ "top_prediction": top3[0]["crop"],
423
+ "confidence_percent": top3[0]["confidence_percent"],
424
+ "confidence_label": top3[0]["confidence_label"],
425
+ "top3": top3,
426
+ "inference_time_ms": inference_ms,
427
+ "model_version": "v6",
428
+ "request_by": client_name,
429
+ }
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.110.0
2
+ uvicorn[standard]==0.29.0
3
+ python-multipart==0.0.9
4
+ tensorflow==2.16.1
5
+ pillow==10.3.0
6
+ numpy==1.26.4
7
+ python-dotenv==1.0.1