Fadhili Sumaye commited on
Commit
573aa2a
·
1 Parent(s): 04dd5f9

Remove login screen and revert to public stateless access with anonymous audit logging

Browse files
app/build.gradle.kts CHANGED
@@ -28,6 +28,10 @@ android {
28
  )
29
  }
30
  }
 
 
 
 
31
  compileOptions {
32
  sourceCompatibility = JavaVersion.VERSION_11
33
  targetCompatibility = JavaVersion.VERSION_11
 
28
  )
29
  }
30
  }
31
+ lint {
32
+ checkReleaseBuilds = false
33
+ abortOnError = false
34
+ }
35
  compileOptions {
36
  sourceCompatibility = JavaVersion.VERSION_11
37
  targetCompatibility = JavaVersion.VERSION_11
app/src/main/AndroidManifest.xml CHANGED
@@ -4,7 +4,6 @@
4
 
5
  <uses-permission android:name="android.permission.INTERNET" />
6
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
7
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
8
  <uses-permission android:name="android.permission.CAMERA" />
9
  <uses-feature android:name="android.hardware.camera" android:required="true" />
10
 
@@ -22,7 +21,7 @@
22
  android:label="@string/app_name"
23
  android:roundIcon="@mipmap/ic_launcher_round"
24
  android:supportsRtl="true"
25
- android:usesCleartextTraffic="true"
26
  android:theme="@style/Theme.PestDetection">
27
 
28
  <activity
@@ -34,6 +33,7 @@
34
  </intent-filter>
35
  </activity>
36
 
 
37
  <provider
38
  android:name="androidx.core.content.FileProvider"
39
  android:authorities="com.example.pestdetection.fileprovider"
 
4
 
5
  <uses-permission android:name="android.permission.INTERNET" />
6
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 
7
  <uses-permission android:name="android.permission.CAMERA" />
8
  <uses-feature android:name="android.hardware.camera" android:required="true" />
9
 
 
21
  android:label="@string/app_name"
22
  android:roundIcon="@mipmap/ic_launcher_round"
23
  android:supportsRtl="true"
24
+ android:networkSecurityConfig="@xml/network_security_config"
25
  android:theme="@style/Theme.PestDetection">
26
 
27
  <activity
 
33
  </intent-filter>
34
  </activity>
35
 
36
+
37
  <provider
38
  android:name="androidx.core.content.FileProvider"
39
  android:authorities="com.example.pestdetection.fileprovider"
app/src/main/java/com/example/pestdetection/ApiConfig.java CHANGED
@@ -27,6 +27,18 @@ public final class ApiConfig {
27
  return toHealthUrl(getPredictUrl(context));
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  public static void savePredictUrl(Context context, String url) {
31
  if (url == null || url.trim().isEmpty()) {
32
  return;
@@ -59,4 +71,11 @@ public final class ApiConfig {
59
  }
60
  return predictUrl + "/health";
61
  }
 
 
 
 
 
 
 
62
  }
 
27
  return toHealthUrl(getPredictUrl(context));
28
  }
29
 
30
+ public static String getLoginUrl(Context context) {
31
+ return toAuthUrl(getPredictUrl(context), "/auth/login");
32
+ }
33
+
34
+ public static String getRegisterUrl(Context context) {
35
+ return toAuthUrl(getPredictUrl(context), "/auth/register");
36
+ }
37
+
38
+ public static String getHistoryUrl(Context context) {
39
+ return toAuthUrl(getPredictUrl(context), "/history");
40
+ }
41
+
42
  public static void savePredictUrl(Context context, String url) {
43
  if (url == null || url.trim().isEmpty()) {
44
  return;
 
71
  }
72
  return predictUrl + "/health";
73
  }
74
+
75
+ private static String toAuthUrl(String predictUrl, String endpoint) {
76
+ if (predictUrl.endsWith("/predict")) {
77
+ return predictUrl.substring(0, predictUrl.length() - "/predict".length()) + endpoint;
78
+ }
79
+ return predictUrl + endpoint;
80
+ }
81
  }
app/src/main/java/com/example/pestdetection/MainActivity.java CHANGED
@@ -27,7 +27,8 @@ import androidx.core.content.FileProvider;
27
  public class MainActivity extends AppCompatActivity {
28
 
29
  ImageView imageView;
30
- TextView resultText, treatmentText, tvConnectionStatus;
 
31
  Uri imageUri;
32
  EditText etServerUrl;
33
 
@@ -70,6 +71,9 @@ public class MainActivity extends AppCompatActivity {
70
  resultText = findViewById(R.id.resultText);
71
  treatmentText = findViewById(R.id.treatmentText);
72
  tvConnectionStatus = findViewById(R.id.tvConnectionStatus);
 
 
 
73
 
74
  etServerUrl = findViewById(R.id.etServerUrl);
75
  View settingsHeader = findViewById(R.id.settingsHeader);
@@ -112,7 +116,6 @@ public class MainActivity extends AppCompatActivity {
112
  @Override
113
  protected void onStop() {
114
  super.onStop();
115
- // Reset UI to default state when the user goes out of the application
116
  if (imageView != null) {
117
  imageView.setImageResource(R.mipmap.ic_launcher);
118
  }
@@ -194,19 +197,31 @@ public class MainActivity extends AppCompatActivity {
194
  }
195
 
196
  private void updateConnectionStatus(Boolean connected, String message) {
197
- if (connected == null) {
198
- tvConnectionStatus.setText("● " + message);
199
- tvConnectionStatus.setTextColor(getColor(R.color.text_hint));
200
  return;
201
  }
202
 
203
- if (connected) {
204
- tvConnectionStatus.setText("● Server connected");
205
- tvConnectionStatus.setTextColor(getColor(R.color.primary_green));
 
 
 
 
 
 
 
 
206
  } else {
207
- tvConnectionStatus.setText("● Offline — " + message);
208
- tvConnectionStatus.setTextColor(getColor(R.color.accent_orange));
 
209
  }
 
 
 
 
 
210
  }
211
 
212
  private void openGallery() {
 
27
  public class MainActivity extends AppCompatActivity {
28
 
29
  ImageView imageView;
30
+ TextView resultText, treatmentText, tvConnectionStatus, tvAppSubtitle;
31
+ View statusBadgeContainer, statusIndicatorDot;
32
  Uri imageUri;
33
  EditText etServerUrl;
34
 
 
71
  resultText = findViewById(R.id.resultText);
72
  treatmentText = findViewById(R.id.treatmentText);
73
  tvConnectionStatus = findViewById(R.id.tvConnectionStatus);
74
+ statusBadgeContainer = findViewById(R.id.statusBadgeContainer);
75
+ statusIndicatorDot = findViewById(R.id.statusIndicatorDot);
76
+ tvAppSubtitle = findViewById(R.id.tvAppSubtitle);
77
 
78
  etServerUrl = findViewById(R.id.etServerUrl);
79
  View settingsHeader = findViewById(R.id.settingsHeader);
 
116
  @Override
117
  protected void onStop() {
118
  super.onStop();
 
119
  if (imageView != null) {
120
  imageView.setImageResource(R.mipmap.ic_launcher);
121
  }
 
197
  }
198
 
199
  private void updateConnectionStatus(Boolean connected, String message) {
200
+ if (statusBadgeContainer == null || statusIndicatorDot == null || tvConnectionStatus == null) {
 
 
201
  return;
202
  }
203
 
204
+ int colorBg, colorDotText;
205
+ String statusText;
206
+
207
+ if (connected == null) {
208
+ colorBg = getColor(R.color.status_gray_bg);
209
+ colorDotText = getColor(R.color.text_hint);
210
+ statusText = "Checking...";
211
+ } else if (connected) {
212
+ colorBg = getColor(R.color.primary_green_light);
213
+ colorDotText = getColor(R.color.primary_green);
214
+ statusText = "Online";
215
  } else {
216
+ colorBg = getColor(R.color.status_red_bg);
217
+ colorDotText = getColor(R.color.status_red_text);
218
+ statusText = "Offline";
219
  }
220
+
221
+ statusBadgeContainer.setBackgroundTintList(android.content.res.ColorStateList.valueOf(colorBg));
222
+ statusIndicatorDot.setBackgroundTintList(android.content.res.ColorStateList.valueOf(colorDotText));
223
+ tvConnectionStatus.setTextColor(colorDotText);
224
+ tvConnectionStatus.setText(statusText);
225
  }
226
 
227
  private void openGallery() {
app/src/main/res/drawable/bg_status_dot.xml ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="oval">
4
+ <solid android:color="#FFFFFF" />
5
+ <size
6
+ android:width="8dp"
7
+ android:height="8dp" />
8
+ </shape>
app/src/main/res/drawable/bg_status_pill.xml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <shape xmlns:android="http://schemas.android.com/apk/res/android"
3
+ android:shape="rectangle">
4
+ <solid android:color="#FFFFFF" />
5
+ <corners android:radius="100dp" />
6
+ </shape>
app/src/main/res/layout/activity_main.xml CHANGED
@@ -35,29 +35,53 @@
35
  app:layout_constraintTop_toBottomOf="@id/tvAppName"
36
  app:layout_constraintStart_toStartOf="parent" />
37
 
38
- <TextView
39
- android:id="@+id/tvConnectionStatus"
 
40
  android:layout_width="wrap_content"
41
  android:layout_height="wrap_content"
42
- android:layout_marginTop="8dp"
43
- android:text="● Checking server..."
44
- android:textColor="@color/text_hint"
45
- android:textSize="12sp"
46
- app:layout_constraintTop_toBottomOf="@id/tvAppSubtitle"
47
- app:layout_constraintStart_toStartOf="parent" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
 
49
  <!-- Server Connection Settings Card -->
50
  <com.google.android.material.card.MaterialCardView
51
  android:id="@+id/settingsCard"
52
  android:layout_width="0dp"
53
  android:layout_height="wrap_content"
54
- android:layout_marginTop="16dp"
55
  app:cardCornerRadius="16dp"
56
  app:cardElevation="2dp"
57
  app:cardBackgroundColor="@color/surface_card"
58
  app:strokeColor="@color/primary_green_light"
59
  app:strokeWidth="1dp"
60
- app:layout_constraintTop_toBottomOf="@id/tvConnectionStatus"
61
  app:layout_constraintStart_toStartOf="parent"
62
  app:layout_constraintEnd_toEndOf="parent">
63
 
 
35
  app:layout_constraintTop_toBottomOf="@id/tvAppName"
36
  app:layout_constraintStart_toStartOf="parent" />
37
 
38
+ <!-- Status Pill Badge -->
39
+ <LinearLayout
40
+ android:id="@+id/statusBadgeContainer"
41
  android:layout_width="wrap_content"
42
  android:layout_height="wrap_content"
43
+ android:orientation="horizontal"
44
+ android:gravity="center_vertical"
45
+ android:paddingStart="12dp"
46
+ android:paddingEnd="12dp"
47
+ android:paddingTop="6dp"
48
+ android:paddingBottom="6dp"
49
+ android:background="@drawable/bg_status_pill"
50
+ app:layout_constraintTop_toTopOf="@id/tvAppName"
51
+ app:layout_constraintBottom_toBottomOf="@id/tvAppSubtitle"
52
+ app:layout_constraintEnd_toEndOf="parent">
53
+
54
+ <View
55
+ android:id="@+id/statusIndicatorDot"
56
+ android:layout_width="8dp"
57
+ android:layout_height="8dp"
58
+ android:layout_marginEnd="6dp"
59
+ android:background="@drawable/bg_status_dot" />
60
+
61
+ <TextView
62
+ android:id="@+id/tvConnectionStatus"
63
+ android:layout_width="wrap_content"
64
+ android:layout_height="wrap_content"
65
+ android:text="Checking..."
66
+ android:textColor="@color/text_hint"
67
+ android:textSize="11sp"
68
+ android:textStyle="bold"
69
+ android:textAllCaps="true"
70
+ android:letterSpacing="0.05" />
71
+ </LinearLayout>
72
 
73
  <!-- Server Connection Settings Card -->
74
  <com.google.android.material.card.MaterialCardView
75
  android:id="@+id/settingsCard"
76
  android:layout_width="0dp"
77
  android:layout_height="wrap_content"
78
+ android:layout_marginTop="20dp"
79
  app:cardCornerRadius="16dp"
80
  app:cardElevation="2dp"
81
  app:cardBackgroundColor="@color/surface_card"
82
  app:strokeColor="@color/primary_green_light"
83
  app:strokeWidth="1dp"
84
+ app:layout_constraintTop_toBottomOf="@id/tvAppSubtitle"
85
  app:layout_constraintStart_toStartOf="parent"
86
  app:layout_constraintEnd_toEndOf="parent">
87
 
app/src/main/res/values/colors.xml CHANGED
@@ -18,4 +18,9 @@
18
  <!-- Standard Colors -->
19
  <color name="black">#000000</color>
20
  <color name="white">#FFFFFF</color>
 
 
 
 
 
21
  </resources>
 
18
  <!-- Standard Colors -->
19
  <color name="black">#000000</color>
20
  <color name="white">#FFFFFF</color>
21
+
22
+ <!-- Status Pill Colors -->
23
+ <color name="status_red_text">#D32F2F</color>
24
+ <color name="status_red_bg">#FFEBEE</color>
25
+ <color name="status_gray_bg">#ECEFF1</color>
26
  </resources>
app/src/main/res/xml/network_security_config.xml ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <network-security-config>
3
+ <!-- Allow cleartext HTTP traffic only for local developer environments -->
4
+ <domain-config cleartextTrafficPermitted="true">
5
+ <domain includeSubdomains="true">localhost</domain>
6
+ <domain includeSubdomains="true">127.0.0.1</domain>
7
+ <domain includeSubdomains="true">10.0.2.2</domain> <!-- Android Emulator loopback to host -->
8
+ </domain-config>
9
+
10
+ <!-- Enforce HTTPS for everything else globally -->
11
+ <base-config cleartextTrafficPermitted="false" />
12
+ </network-security-config>
backend/app.py CHANGED
@@ -1,21 +1,53 @@
1
- from fastapi import FastAPI, File, UploadFile, HTTPException
2
  from fastapi.responses import PlainTextResponse
3
  import os
4
- import shutil
5
  from pathlib import Path
6
- from werkzeug.utils import secure_filename
7
  import threading
8
  import time
9
  import urllib.request
 
 
 
 
 
10
 
11
  BASE_DIR = Path(__file__).resolve().parent
12
- UPLOAD_FOLDER = BASE_DIR / "uploads"
13
  MODEL_FILE = BASE_DIR / "best_cereal.pt"
14
  MODEL_IP102_FILE = BASE_DIR / "best_ip102.pt"
15
  IP102_URL = "https://huggingface.co/underdogquality/yolo11s-pest-detection/resolve/main/best.pt"
16
- ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
17
 
18
- UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  app = FastAPI(title="Pest Detection API")
21
 
@@ -37,7 +69,7 @@ def download_ip102_model():
37
  temp_file = MODEL_IP102_FILE.with_suffix(".tmp")
38
 
39
  with urllib.request.urlopen(req, timeout=60) as response, open(temp_file, 'wb') as out_file:
40
- chunk_size = 1024 * 1024 # 1MB chunks
41
  bytes_downloaded = 0
42
  while True:
43
  chunk = response.read(chunk_size)
@@ -77,14 +109,12 @@ def init_model():
77
  try:
78
  from ultralytics import YOLO
79
 
80
- # 1. Look for IP102 model
81
  if MODEL_IP102_FILE.exists():
82
  model = YOLO(str(MODEL_IP102_FILE))
83
  HAS_YOLO = True
84
  print(f"Success: Loaded IP102 YOLO model from {MODEL_IP102_FILE} ({len(model.names)} classes)")
85
  return
86
 
87
- # 2. Try loading a fallback model to serve requests in the meantime
88
  loaded_fallback = False
89
  if MODEL_FILE.exists():
90
  model = YOLO(str(MODEL_FILE))
@@ -99,15 +129,14 @@ def init_model():
99
  print(f"Success: Loaded fallback YOLO model from {fallback_path} ({len(model.names)} classes)")
100
  loaded_fallback = True
101
 
102
- # 3. Always trigger background download of IP102 if it is missing
103
- print("IP102 model is missing. Initiating background download for IP102 model to include all 102 pests...")
104
  threading.Thread(target=download_ip102_model, daemon=True).start()
105
  except Exception as e:
106
  if not HAS_YOLO:
107
  HAS_YOLO = False
108
  print(f"Warning: Failed to load YOLO model: {e}. Using dummy detection.")
109
  else:
110
- print(f"Warning: Fallback model loaded, but error occurred during startup sequence: {e}")
111
 
112
  init_model()
113
 
@@ -132,7 +161,7 @@ def get_ip102_category_and_treatment(cls_id, raw_name):
132
  elif 73 <= cls_id <= 91:
133
  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."
134
  elif 92 <= cls_id <= 101:
135
- 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 flushing/flowering."
136
  return "Unknown Pest", "Consult with a local agricultural officer for specific treatment advice."
137
 
138
  PEST_TREATMENT = {
@@ -158,6 +187,8 @@ PEST_TREATMENT = {
158
  "default": "Consult with a local agricultural officer for specific treatment advice.",
159
  }
160
 
 
 
161
  def allowed_file(filename):
162
  return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
163
 
@@ -169,13 +200,13 @@ def index():
169
  return f"Pest Detection API is running! YOLO Model: {model_info}"
170
 
171
  @app.get("/health")
172
- def health():
173
- model_type = "None"
174
  classes_count = 0
 
175
  if HAS_YOLO and model:
176
  classes_count = len(model.names)
177
  model_type = "IP102 (102 pests)" if classes_count == 102 else "Cereal Pests (10 pests)"
178
-
179
  return {
180
  "status": "ok",
181
  "yolo": HAS_YOLO,
@@ -185,25 +216,37 @@ def health():
185
  }
186
 
187
  @app.post("/predict")
188
- async def predict(image: UploadFile = File(...)):
 
 
 
 
 
189
  if not image.filename:
 
190
  raise HTTPException(status_code=400, detail="No selected file")
191
 
192
  if not allowed_file(image.filename):
 
193
  raise HTTPException(status_code=400, detail="File type not allowed")
194
 
195
- filename = secure_filename(image.filename)
196
- filepath = UPLOAD_FOLDER / filename
197
-
198
  try:
199
- with open(filepath, "wb") as buffer:
200
- shutil.copyfileobj(image.file, buffer)
 
 
 
 
 
 
 
201
  except Exception as e:
202
- raise HTTPException(status_code=500, detail=f"Failed to save file: {str(e)}")
 
203
 
204
  if HAS_YOLO:
205
  try:
206
- results = model.predict(source=str(filepath), save=False, conf=0.4)
207
 
208
  detections = []
209
  for r in results:
@@ -226,10 +269,10 @@ async def predict(image: UploadFile = File(...)):
226
  "confidence": round(conf, 2),
227
  "treatment": treatment,
228
  })
229
-
230
- print(f"Detections for {filename}: {detections}")
231
 
232
  if not detections:
 
 
233
  return {
234
  "status": "success",
235
  "pest_detected": "None",
@@ -238,6 +281,13 @@ async def predict(image: UploadFile = File(...)):
238
  "message": "No pests detected",
239
  }
240
 
 
 
 
 
 
 
 
241
  return {
242
  "status": "success",
243
  "pest_detected": detections[0]["pest_detected"],
@@ -247,8 +297,11 @@ async def predict(image: UploadFile = File(...)):
247
  "all_detections": detections,
248
  }
249
  except Exception as e:
 
250
  raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
251
  else:
 
 
252
  return {
253
  "status": "success",
254
  "pest_detected": "Aphids (Mock)",
@@ -259,6 +312,5 @@ async def predict(image: UploadFile = File(...)):
259
 
260
  if __name__ == "__main__":
261
  import uvicorn
262
-
263
  port = int(os.environ.get("PORT", 5000))
264
  uvicorn.run(app, host="0.0.0.0", port=port)
 
1
+ from fastapi import FastAPI, File, UploadFile, HTTPException, Request
2
  from fastapi.responses import PlainTextResponse
3
  import os
 
4
  from pathlib import Path
 
5
  import threading
6
  import time
7
  import urllib.request
8
+ import sqlite3
9
+ import json
10
+ import io
11
+ from PIL import Image
12
+ from typing import Optional
13
 
14
  BASE_DIR = Path(__file__).resolve().parent
15
+ DB_FILE = BASE_DIR / "pest_detection.db"
16
  MODEL_FILE = BASE_DIR / "best_cereal.pt"
17
  MODEL_IP102_FILE = BASE_DIR / "best_ip102.pt"
18
  IP102_URL = "https://huggingface.co/underdogquality/yolo11s-pest-detection/resolve/main/best.pt"
 
19
 
20
+ def init_db():
21
+ conn = sqlite3.connect(str(DB_FILE))
22
+ cursor = conn.cursor()
23
+ cursor.execute("""
24
+ CREATE TABLE IF NOT EXISTS audit_logs (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ username TEXT,
27
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
28
+ endpoint TEXT NOT NULL,
29
+ status TEXT NOT NULL,
30
+ details TEXT,
31
+ ip_address TEXT
32
+ )
33
+ """)
34
+ conn.commit()
35
+ conn.close()
36
+
37
+ def log_audit(username: Optional[str], endpoint: str, status: str, details: str, ip_address: str):
38
+ try:
39
+ conn = sqlite3.connect(str(DB_FILE))
40
+ cursor = conn.cursor()
41
+ cursor.execute(
42
+ "INSERT INTO audit_logs (username, endpoint, status, details, ip_address) VALUES (?, ?, ?, ?, ?)",
43
+ (username, endpoint, status, details, ip_address)
44
+ )
45
+ conn.commit()
46
+ conn.close()
47
+ except Exception as e:
48
+ print(f"Failed to write audit log: {e}")
49
+
50
+ init_db()
51
 
52
  app = FastAPI(title="Pest Detection API")
53
 
 
69
  temp_file = MODEL_IP102_FILE.with_suffix(".tmp")
70
 
71
  with urllib.request.urlopen(req, timeout=60) as response, open(temp_file, 'wb') as out_file:
72
+ chunk_size = 1024 * 1024
73
  bytes_downloaded = 0
74
  while True:
75
  chunk = response.read(chunk_size)
 
109
  try:
110
  from ultralytics import YOLO
111
 
 
112
  if MODEL_IP102_FILE.exists():
113
  model = YOLO(str(MODEL_IP102_FILE))
114
  HAS_YOLO = True
115
  print(f"Success: Loaded IP102 YOLO model from {MODEL_IP102_FILE} ({len(model.names)} classes)")
116
  return
117
 
 
118
  loaded_fallback = False
119
  if MODEL_FILE.exists():
120
  model = YOLO(str(MODEL_FILE))
 
129
  print(f"Success: Loaded fallback YOLO model from {fallback_path} ({len(model.names)} classes)")
130
  loaded_fallback = True
131
 
132
+ print("IP102 model is missing. Initiating background download...")
 
133
  threading.Thread(target=download_ip102_model, daemon=True).start()
134
  except Exception as e:
135
  if not HAS_YOLO:
136
  HAS_YOLO = False
137
  print(f"Warning: Failed to load YOLO model: {e}. Using dummy detection.")
138
  else:
139
+ print(f"Warning: Fallback model loaded, but error occurred: {e}")
140
 
141
  init_model()
142
 
 
161
  elif 73 <= cls_id <= 91:
162
  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."
163
  elif 92 <= cls_id <= 101:
164
+ 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."
165
  return "Unknown Pest", "Consult with a local agricultural officer for specific treatment advice."
166
 
167
  PEST_TREATMENT = {
 
187
  "default": "Consult with a local agricultural officer for specific treatment advice.",
188
  }
189
 
190
+ ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg"}
191
+
192
  def allowed_file(filename):
193
  return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
194
 
 
200
  return f"Pest Detection API is running! YOLO Model: {model_info}"
201
 
202
  @app.get("/health")
203
+ def health(request: Request):
 
204
  classes_count = 0
205
+ model_type = "None"
206
  if HAS_YOLO and model:
207
  classes_count = len(model.names)
208
  model_type = "IP102 (102 pests)" if classes_count == 102 else "Cereal Pests (10 pests)"
209
+
210
  return {
211
  "status": "ok",
212
  "yolo": HAS_YOLO,
 
216
  }
217
 
218
  @app.post("/predict")
219
+ async def predict(
220
+ request: Request,
221
+ image: UploadFile = File(...)
222
+ ):
223
+ ip = request.client.host if request.client else "unknown"
224
+
225
  if not image.filename:
226
+ log_audit("anonymous", "/predict", "failed", "No selected file", ip)
227
  raise HTTPException(status_code=400, detail="No selected file")
228
 
229
  if not allowed_file(image.filename):
230
+ log_audit("anonymous", "/predict", "failed", f"File type not allowed: {image.filename}", ip)
231
  raise HTTPException(status_code=400, detail="File type not allowed")
232
 
 
 
 
233
  try:
234
+ contents = await image.read()
235
+
236
+ if len(contents) > 5 * 1024 * 1024:
237
+ log_audit("anonymous", "/predict", "failed", "File too large (exceeded 5MB)", ip)
238
+ raise HTTPException(status_code=413, detail="File size exceeds the 5MB limit.")
239
+
240
+ img = Image.open(io.BytesIO(contents)).convert("RGB")
241
+ except HTTPException as he:
242
+ raise he
243
  except Exception as e:
244
+ log_audit("anonymous", "/predict", "error", f"Image parsing error: {str(e)}", ip)
245
+ raise HTTPException(status_code=500, detail=f"Failed to process image: {str(e)}")
246
 
247
  if HAS_YOLO:
248
  try:
249
+ results = model.predict(source=img, save=False, conf=0.4)
250
 
251
  detections = []
252
  for r in results:
 
269
  "confidence": round(conf, 2),
270
  "treatment": treatment,
271
  })
 
 
272
 
273
  if not detections:
274
+ log_details = json.dumps({"pest": "None", "confidence": 0.0, "filename": image.filename})
275
+ log_audit("anonymous", "/predict", "success", log_details, ip)
276
  return {
277
  "status": "success",
278
  "pest_detected": "None",
 
281
  "message": "No pests detected",
282
  }
283
 
284
+ log_details = json.dumps({
285
+ "pest": detections[0]["pest_detected"],
286
+ "confidence": detections[0]["confidence"],
287
+ "filename": image.filename
288
+ })
289
+ log_audit("anonymous", "/predict", "success", log_details, ip)
290
+
291
  return {
292
  "status": "success",
293
  "pest_detected": detections[0]["pest_detected"],
 
297
  "all_detections": detections,
298
  }
299
  except Exception as e:
300
+ log_audit("anonymous", "/predict", "error", f"YOLO Inference error: {str(e)}", ip)
301
  raise HTTPException(status_code=500, detail=f"Inference error: {str(e)}")
302
  else:
303
+ log_details = json.dumps({"pest": "Aphids (Mock)", "confidence": 0.95, "filename": image.filename})
304
+ log_audit("anonymous", "/predict", "success", log_details, ip)
305
  return {
306
  "status": "success",
307
  "pest_detected": "Aphids (Mock)",
 
312
 
313
  if __name__ == "__main__":
314
  import uvicorn
 
315
  port = int(os.environ.get("PORT", 5000))
316
  uvicorn.run(app, host="0.0.0.0", port=port)
gradle.properties CHANGED
@@ -20,4 +20,7 @@ kotlin.code.style=official
20
  # Enables namespacing of each library's R class so that its R class includes only the
21
  # resources declared in the library itself and none from the library's dependencies,
22
  # thereby reducing the size of the R class for that library
23
- android.nonTransitiveRClass=true
 
 
 
 
20
  # Enables namespacing of each library's R class so that its R class includes only the
21
  # resources declared in the library itself and none from the library's dependencies,
22
  # thereby reducing the size of the R class for that library
23
+ android.nonTransitiveRClass=true
24
+
25
+ # Java Home configuration for Gradle build
26
+ org.gradle.java.home=C:\\Program Files\\Android\\Android Studio\\jbr
gradlew.bat CHANGED
@@ -39,6 +39,14 @@ for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
39
  set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40
 
41
  @rem Find java.exe
 
 
 
 
 
 
 
 
42
  if defined JAVA_HOME goto findJavaFromJavaHome
43
 
44
  set JAVA_EXE=java.exe
 
39
  set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40
 
41
  @rem Find java.exe
42
+ if not defined JAVA_HOME (
43
+ if exist "C:\Program Files\Android\Android Studio\jbr" (
44
+ set "JAVA_HOME=C:\Program Files\Android\Android Studio\jbr"
45
+ ) else if exist "C:\Program Files\Android\Android Studio\jre" (
46
+ set "JAVA_HOME=C:\Program Files\Android\Android Studio\jre"
47
+ )
48
+ )
49
+
50
  if defined JAVA_HOME goto findJavaFromJavaHome
51
 
52
  set JAVA_EXE=java.exe