Ayatullah-hanif commited on
Commit
5eda14f
·
1 Parent(s): f0a0646

fix huggingface config

Browse files
Files changed (1) hide show
  1. app_fastapi.py +127 -167
app_fastapi.py CHANGED
@@ -4,36 +4,29 @@ AgriCare – Disease Detection API
4
 
5
  Production-grade AI backend for cassava disease detection.
6
 
7
- Features:
8
  ✓ ONNX EfficientNet-B3 inference
9
- Softmax-calibrated probabilities
10
- Multilingual explanations (EN / HA / IG / YO)
11
- ✓ Responsible AI fallback
12
  ✓ Human-in-the-loop escalation
13
-
14
- Designed for real-world Nigerian agriculture.
15
  """
16
 
17
  import os
18
  import io
19
  import logging
20
- from datetime import datetime
21
-
22
  import numpy as np
23
  import onnxruntime as ort
24
- import torch
25
  import uvicorn
26
  from PIL import Image
27
- from transformers import AutoTokenizer, AutoModelForCausalLM
28
-
29
  from fastapi import FastAPI, UploadFile, File, Form
30
  from fastapi.middleware.cors import CORSMiddleware
31
  from fastapi.responses import HTMLResponse
32
 
33
-
34
- # =====================================================
35
  # App Setup
36
- # =====================================================
37
  app = FastAPI(title="AgriCare Disease Detection API")
38
 
39
  app.add_middleware(
@@ -47,21 +40,14 @@ app.add_middleware(
47
  logging.basicConfig(level=logging.INFO)
48
  logger = logging.getLogger("agricare_api")
49
 
50
-
51
- # =====================================================
52
- # ONNX Model Setup
53
- # =====================================================
54
  MODEL_PATH = "cassava_efficientnetb3_fp16.onnx"
55
-
56
- sess = ort.InferenceSession(
57
- MODEL_PATH,
58
- providers=["CPUExecutionProvider"]
59
- )
60
 
61
  INPUT_NAME = sess.get_inputs()[0].name
62
- MODEL_DTYPE = (
63
- np.float16 if "float16" in sess.get_inputs()[0].type else np.float32
64
- )
65
 
66
  CLASS_NAMES = [
67
  "Cassava Bacterial Blight",
@@ -74,155 +60,126 @@ CLASS_NAMES = [
74
  IMG_SIZE = 300
75
  LOW_CONF_THRESHOLD = 0.60
76
 
77
-
78
- # =====================================================
79
- # Hugging Face – N-ATLaS (Local Model)
80
- # =====================================================
81
- HF_TOKEN = os.getenv("HF_TOKEN")
82
- NATLAS_MODEL_NAME = "NCAIR1/N-ATLaS"
83
-
84
- tokenizer = None
85
- llm_model = None
86
-
87
- logger.info(f"Loading N-ATLaS model: {NATLAS_MODEL_NAME}")
88
-
89
- try:
90
- tokenizer = AutoTokenizer.from_pretrained(
91
- NATLAS_MODEL_NAME,
92
- token=HF_TOKEN
 
 
 
 
 
 
 
93
  )
94
- llm_model = AutoModelForCausalLM.from_pretrained(
95
- NATLAS_MODEL_NAME,
96
- torch_dtype=torch.float16,
97
- device_map="cpu",
98
- token=HF_TOKEN
99
- )
100
- logger.info("N-ATLaS model loaded successfully.")
101
- except Exception as e:
102
- logger.error(f"N-ATLaS load failed: {e}")
103
-
104
 
105
- # =====================================================
106
- # Helper Functions
107
- # =====================================================
108
- def softmax(x: np.ndarray) -> np.ndarray:
109
- e_x = np.exp(x - np.max(x))
110
- return e_x / e_x.sum()
111
-
112
-
113
- def preprocess(image_bytes: bytes) -> np.ndarray:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
115
  img = img.resize((IMG_SIZE, IMG_SIZE))
116
 
117
  arr = np.array(img).astype("float32") / 255.0
118
  arr = (arr - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
119
  arr = np.transpose(arr, (2, 0, 1))
120
-
121
  return arr[np.newaxis, :].astype(MODEL_DTYPE)
122
 
 
 
 
 
 
 
123
 
124
- def format_chat_prompt(messages: list) -> str:
125
- if tokenizer is None:
126
- return ""
127
 
128
- return tokenizer.apply_chat_template(
129
- messages,
130
- add_generation_prompt=True,
131
- tokenize=False,
132
- date_string=datetime.now().strftime("%d %b %Y")
133
- )
134
-
135
-
136
- def generate_text_explanation(predicted_class: str) -> str:
137
- if llm_model is None or tokenizer is None:
138
- logger.warning("N-ATLaS unavailable — using fallback.")
139
- return ""
140
 
141
- messages = [
142
- {
143
- "role": "system",
144
- "content": (
145
- "You are an agricultural extension officer providing advice "
146
- "in English, Hausa, Igbo, and Yoruba."
147
- )
148
- },
149
- {
150
- "role": "user",
151
- "content": (
152
- f"Provide short, farmer-friendly advice for the condition: "
153
- f"{predicted_class}. Format exactly as:\n"
154
- "English:\nHausa:\nIgbo:\nYoruba:"
155
- )
156
- }
157
- ]
158
-
159
- prompt = format_chat_prompt(messages)
160
- inputs = tokenizer(
161
- prompt,
162
- return_tensors="pt",
163
- add_special_tokens=False
164
  )
165
 
166
- inputs = {k: v.to("cpu") for k, v in inputs.items()}
167
-
168
- outputs = llm_model.generate(
169
- **inputs,
170
- max_new_tokens=512,
171
- temperature=0.1,
172
- repetition_penalty=1.12,
173
- use_cache=True
 
 
 
 
 
 
 
 
 
 
174
  )
175
 
176
- decoded = tokenizer.decode(outputs[0], skip_special_tokens=True)
177
- return decoded.replace(prompt, "").strip()
178
-
179
-
180
- def extract_sections(text: str) -> dict:
181
- sections = {"english": "", "hausa": "", "igbo": "", "yoruba": ""}
182
- current = None
183
-
184
- for line in text.splitlines():
185
- key = line.lower().strip()
186
 
187
- if key.startswith("english"):
188
- current = "english"; continue
189
- if key.startswith("hausa"):
190
- current = "hausa"; continue
191
- if key.startswith("igbo"):
192
- current = "igbo"; continue
193
- if key.startswith("yoruba"):
194
- current = "yoruba"; continue
195
-
196
- if current:
197
- sections[current] += line.strip() + " "
198
-
199
- if not any(sections.values()):
200
- sections["english"] = (
201
- "This disease was detected with high confidence. "
202
- "Please consult a trained agricultural extension officer "
203
- "for proper treatment and prevention."
204
- )
205
-
206
- return sections
207
-
208
-
209
- # =====================================================
210
- # API Routes
211
- # =====================================================
212
  @app.get("/", response_class=HTMLResponse)
213
  def root():
214
- return """
215
- <html>
216
- <head><title>AgriCare API</title></head>
217
- <body>
218
- <h1>AgriCare API is running</h1>
219
- <p>Use <a href="/docs">/docs</a> for API documentation.</p>
220
- <p>POST images to <b>/predict</b></p>
221
- </body>
222
- </html>
223
- """
224
-
225
 
 
 
 
226
  @app.post("/predict")
227
  async def predict(
228
  file: UploadFile = File(...),
@@ -236,27 +193,30 @@ async def predict(
236
 
237
  idx = int(np.argmax(probs))
238
  confidence = float(probs[idx])
239
- predicted = CLASS_NAMES[idx]
240
 
241
- raw_text = generate_text_explanation(predicted)
242
- sections = extract_sections(raw_text)
243
 
244
- explanation = sections.get(language.lower(), "").strip()
245
- if not explanation:
246
- explanation = sections["english"]
 
 
247
 
248
  return {
249
  "status": "low_confidence" if confidence < LOW_CONF_THRESHOLD else "ok",
250
- "predicted_class": predicted,
251
  "confidence": round(confidence, 4),
252
  "route_to_expert": confidence < LOW_CONF_THRESHOLD,
253
  "language": language,
254
- "explanation": explanation,
 
255
  "probabilities": probs.tolist()
256
  }
257
 
258
-
259
- # =====================================================
260
- # Local Run
261
- # =====================================================
262
- # uvicorn.run("app_fastapi:app", host="0.0.0.0", port=8000, reload=True)
 
4
 
5
  Production-grade AI backend for cassava disease detection.
6
 
 
7
  ✓ ONNX EfficientNet-B3 inference
8
+ Deterministic English advice (dict-based)
9
+ N-ATLaS translation + audio generation
 
10
  ✓ Human-in-the-loop escalation
11
+ ✓ Hugging Face Spaces ready
 
12
  """
13
 
14
  import os
15
  import io
16
  import logging
17
+ import base64
 
18
  import numpy as np
19
  import onnxruntime as ort
20
+ import requests
21
  import uvicorn
22
  from PIL import Image
 
 
23
  from fastapi import FastAPI, UploadFile, File, Form
24
  from fastapi.middleware.cors import CORSMiddleware
25
  from fastapi.responses import HTMLResponse
26
 
27
+ # -------------------------------------------------------
 
28
  # App Setup
29
+ # -------------------------------------------------------
30
  app = FastAPI(title="AgriCare Disease Detection API")
31
 
32
  app.add_middleware(
 
40
  logging.basicConfig(level=logging.INFO)
41
  logger = logging.getLogger("agricare_api")
42
 
43
+ # -------------------------------------------------------
44
+ # Model Setup
45
+ # -------------------------------------------------------
 
46
  MODEL_PATH = "cassava_efficientnetb3_fp16.onnx"
47
+ sess = ort.InferenceSession(MODEL_PATH, providers=["CPUExecutionProvider"])
 
 
 
 
48
 
49
  INPUT_NAME = sess.get_inputs()[0].name
50
+ MODEL_DTYPE = np.float16 if "float16" in sess.get_inputs()[0].type else np.float32
 
 
51
 
52
  CLASS_NAMES = [
53
  "Cassava Bacterial Blight",
 
60
  IMG_SIZE = 300
61
  LOW_CONF_THRESHOLD = 0.60
62
 
63
+ # -------------------------------------------------------
64
+ # English Advice Dictionary (SOURCE OF TRUTH)
65
+ # -------------------------------------------------------
66
+ ENGLISH_ADVICE = {
67
+ "Cassava Bacterial Blight": (
68
+ "Cassava bacterial blight causes leaf wilting and stem rot. "
69
+ "Remove infected plants, avoid overhead watering, and use resistant varieties."
70
+ ),
71
+ "Cassava Brown Streak Disease": (
72
+ "Cassava brown streak disease damages roots and reduces yield. "
73
+ "Use certified disease-free cuttings and control whiteflies."
74
+ ),
75
+ "Cassava Green Mottle": (
76
+ "Cassava green mottle causes leaf discoloration. "
77
+ "Remove affected plants early and maintain good farm hygiene."
78
+ ),
79
+ "Cassava Mosaic Disease": (
80
+ "Cassava mosaic disease leads to distorted leaves and stunted growth. "
81
+ "Plant resistant varieties and control whitefly populations."
82
+ ),
83
+ "Healthy Leaf": (
84
+ "Your cassava plant appears healthy. "
85
+ "Continue good farming practices and monitor regularly."
86
  )
87
+ }
 
 
 
 
 
 
 
 
 
88
 
89
+ # -------------------------------------------------------
90
+ # Hugging Face – N-ATLaS
91
+ # -------------------------------------------------------
92
+ HF_TOKEN = os.getenv("HF_TOKEN")
93
+ if not HF_TOKEN:
94
+ raise RuntimeError("HF_TOKEN is required and must be set in Hugging Face Secrets.")
95
+
96
+ HF_BASE = "https://router.huggingface.co/hf-inference/models"
97
+ NATLAS_TEXT_URL = f"{HF_BASE}/NCAIR1/N-ATLaS"
98
+ NATLAS_TTS_URL = f"{HF_BASE}/NCAIR1/N-ATLaS-TTS"
99
+
100
+ HEADERS = {
101
+ "Authorization": f"Bearer {HF_TOKEN}",
102
+ "Content-Type": "application/json"
103
+ }
104
+
105
+ LANG_CODE_MAP = {
106
+ "english": "en",
107
+ "hausa": "ha",
108
+ "igbo": "ig",
109
+ "yoruba": "yo"
110
+ }
111
+
112
+ # -------------------------------------------------------
113
+ # Utilities
114
+ # -------------------------------------------------------
115
+ def softmax(x):
116
+ e = np.exp(x - np.max(x))
117
+ return e / e.sum()
118
+
119
+ def preprocess(image_bytes):
120
  img = Image.open(io.BytesIO(image_bytes)).convert("RGB")
121
  img = img.resize((IMG_SIZE, IMG_SIZE))
122
 
123
  arr = np.array(img).astype("float32") / 255.0
124
  arr = (arr - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
125
  arr = np.transpose(arr, (2, 0, 1))
 
126
  return arr[np.newaxis, :].astype(MODEL_DTYPE)
127
 
128
+ # -------------------------------------------------------
129
+ # N-ATLaS Translation
130
+ # -------------------------------------------------------
131
+ def translate_text(text: str, target_language: str) -> str:
132
+ if target_language == "english":
133
+ return text
134
 
135
+ prompt = f"""
136
+ Translate the following agricultural advice into {target_language}.
137
+ Keep it clear and farmer-friendly.
138
 
139
+ Text:
140
+ {text}
141
+ """
 
 
 
 
 
 
 
 
 
142
 
143
+ r = requests.post(
144
+ NATLAS_TEXT_URL,
145
+ headers=HEADERS,
146
+ json={"inputs": prompt},
147
+ timeout=25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  )
149
 
150
+ r.raise_for_status()
151
+ data = r.json()
152
+ return data[0]["generated_text"]
153
+
154
+ # -------------------------------------------------------
155
+ # N-ATLaS Audio
156
+ # -------------------------------------------------------
157
+ def generate_audio(text: str, language: str):
158
+ lang_code = LANG_CODE_MAP.get(language, "en")
159
+
160
+ r = requests.post(
161
+ NATLAS_TTS_URL,
162
+ headers=HEADERS,
163
+ json={
164
+ "inputs": text,
165
+ "parameters": {"language": lang_code}
166
+ },
167
+ timeout=25
168
  )
169
 
170
+ r.raise_for_status()
171
+ return r.json().get("audio") # base64 WAV
 
 
 
 
 
 
 
 
172
 
173
+ # -------------------------------------------------------
174
+ # Root (HF requirement)
175
+ # -------------------------------------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  @app.get("/", response_class=HTMLResponse)
177
  def root():
178
+ return "<h2>AgriCare API is running</h2><p>Visit <a href='/docs'>/docs</a></p>"
 
 
 
 
 
 
 
 
 
 
179
 
180
+ # -------------------------------------------------------
181
+ # Prediction Endpoint
182
+ # -------------------------------------------------------
183
  @app.post("/predict")
184
  async def predict(
185
  file: UploadFile = File(...),
 
193
 
194
  idx = int(np.argmax(probs))
195
  confidence = float(probs[idx])
196
+ disease = CLASS_NAMES[idx]
197
 
198
+ # English source text
199
+ english_text = ENGLISH_ADVICE[disease]
200
 
201
+ # Translation
202
+ final_text = translate_text(english_text, language.lower())
203
+
204
+ # Audio
205
+ audio = generate_audio(final_text, language.lower())
206
 
207
  return {
208
  "status": "low_confidence" if confidence < LOW_CONF_THRESHOLD else "ok",
209
+ "predicted_class": disease,
210
  "confidence": round(confidence, 4),
211
  "route_to_expert": confidence < LOW_CONF_THRESHOLD,
212
  "language": language,
213
+ "text": final_text,
214
+ "audio_base64": audio,
215
  "probabilities": probs.tolist()
216
  }
217
 
218
+ # -------------------------------------------------------
219
+ # Run (Local)
220
+ # -------------------------------------------------------
221
+ if __name__ == "__main__":
222
+ uvicorn.run("app_fastapi:app", host="0.0.0.0", port=8000, reload=True)