yasyn14 commited on
Commit
82a8158
·
1 Parent(s): b451240

adjusted main

Browse files
Files changed (3) hide show
  1. disease_guide.json +38 -38
  2. main.py +237 -108
  3. requirements.txt +0 -0
disease_guide.json CHANGED
@@ -1,5 +1,5 @@
1
  {
2
- "Apple___Apple_scab": {
3
  "disease_name": "Apple Scab",
4
  "common_names": ["Apple scab"],
5
  "crop": "Apple",
@@ -34,7 +34,7 @@
34
  }
35
  ]
36
  },
37
- "Apple___Black_rot": {
38
  "disease_name": "Black Rot",
39
  "common_names": ["Apple black rot"],
40
  "crop": "Apple",
@@ -60,7 +60,7 @@
60
  "type": "Fungal",
61
  "external_resources": []
62
  },
63
- "Apple___Cedar_apple_rust": {
64
  "disease_name": "Cedar Apple Rust",
65
  "common_names": ["Cedar rust"],
66
  "crop": "Apple",
@@ -86,7 +86,7 @@
86
  "type": "Fungal",
87
  "external_resources": []
88
  },
89
- "Apple___healthy": {
90
  "disease_name": null,
91
  "common_names": [],
92
  "crop": "Apple",
@@ -105,7 +105,7 @@
105
  "type": "None",
106
  "external_resources": []
107
  },
108
- "Tomato___Late_blight": {
109
  "disease_name": "Late Blight",
110
  "common_names": ["Tomato late blight"],
111
  "crop": "Tomato",
@@ -130,7 +130,7 @@
130
  "type": "Fungal",
131
  "external_resources": []
132
  },
133
- "Blueberry__healthy": {
134
  "disease_name": null,
135
  "common_names": [],
136
  "crop": "Blueberry",
@@ -149,7 +149,7 @@
149
  "type": "None",
150
  "external_resources": []
151
  },
152
- "Cherry_(including_sour)__Powdery_mildew": {
153
  "disease_name": "Powdery Mildew",
154
  "common_names": ["Cherry powdery mildew"],
155
  "crop": "Cherry (including sour)",
@@ -178,7 +178,7 @@
178
  "type": "Fungal",
179
  "external_resources": []
180
  },
181
- "Cherry_(including_sour)__healthy": {
182
  "disease_name": null,
183
  "common_names": [],
184
  "crop": "Cherry (including sour)",
@@ -197,7 +197,7 @@
197
  "type": "None",
198
  "external_resources": []
199
  },
200
- "Corn_(maize)___Cercospora_leaf_spot_Gray_leaf_spot": {
201
  "disease_name": "Gray Leaf Spot",
202
  "common_names": ["Cercospora Leaf Spot", "Gray Leaf Spot"],
203
  "crop": "Corn (maize)",
@@ -225,7 +225,7 @@
225
  "type": "Fungal",
226
  "external_resources": []
227
  },
228
- "Corn_(maize)___Common_rust": {
229
  "disease_name": "Common Rust",
230
  "common_names": ["Corn Rust"],
231
  "crop": "Corn (maize)",
@@ -250,7 +250,7 @@
250
  "type": "Fungal",
251
  "external_resources": []
252
  },
253
- "Corn_(maize)___Northern_Leaf_Blight": {
254
  "disease_name": "Northern Leaf Blight",
255
  "common_names": ["NLB"],
256
  "crop": "Corn (maize)",
@@ -275,7 +275,7 @@
275
  "type": "Fungal",
276
  "external_resources": []
277
  },
278
- "Corn_(maize)___healthy": {
279
  "disease_name": null,
280
  "common_names": [],
281
  "crop": "Corn (maize)",
@@ -294,7 +294,7 @@
294
  "type": "None",
295
  "external_resources": []
296
  },
297
- "Grape___Black_rot": {
298
  "disease_name": "Black Rot",
299
  "common_names": ["Grape Black Rot"],
300
  "crop": "Grape",
@@ -319,7 +319,7 @@
319
  "type": "Fungal",
320
  "external_resources": []
321
  },
322
- "Grape___Esca_(Black_Measles)": {
323
  "disease_name": "Esca (Black Measles)",
324
  "common_names": ["Esca", "Black Measles"],
325
  "crop": "Grape",
@@ -342,7 +342,7 @@
342
  "type": "Fungal",
343
  "external_resources": []
344
  },
345
- "Grape___Leaf_blight_(Isariopsis_Leaf_Spot)": {
346
  "disease_name": "Leaf Blight (Isariopsis Leaf Spot)",
347
  "common_names": ["Isariopsis Leaf Spot"],
348
  "crop": "Grape",
@@ -367,7 +367,7 @@
367
  "type": "Fungal",
368
  "external_resources": []
369
  },
370
- "Grape___healthy": {
371
  "disease_name": null,
372
  "common_names": [],
373
  "crop": "Grape",
@@ -386,7 +386,7 @@
386
  "type": "None",
387
  "external_resources": []
388
  },
389
- "Orange___Haunglongbing_(Citrus_greening)": {
390
  "disease_name": "Huanglongbing (Citrus Greening)",
391
  "common_names": ["Citrus Greening", "HLB"],
392
  "crop": "Orange",
@@ -417,7 +417,7 @@
417
  }
418
  ]
419
  },
420
- "Peach___Bacterial_spot": {
421
  "disease_name": "Bacterial Spot",
422
  "common_names": ["Peach Bacterial Spot"],
423
  "crop": "Peach",
@@ -443,7 +443,7 @@
443
  "type": "Bacterial",
444
  "external_resources": []
445
  },
446
- "Peach___healthy": {
447
  "disease_name": null,
448
  "common_names": [],
449
  "crop": "Peach",
@@ -462,7 +462,7 @@
462
  "type": "None",
463
  "external_resources": []
464
  },
465
- "Pepper,_bell___Bacterial_spot": {
466
  "disease_name": "Bacterial Spot",
467
  "common_names": ["Bell Pepper Bacterial Spot"],
468
  "crop": "Pepper, bell",
@@ -490,7 +490,7 @@
490
  "type": "Bacterial",
491
  "external_resources": []
492
  },
493
- "Pepper,_bell___healthy": {
494
  "disease_name": null,
495
  "common_names": [],
496
  "crop": "Pepper, bell",
@@ -511,7 +511,7 @@
511
  "type": "None",
512
  "external_resources": []
513
  },
514
- "Potato___Early_blight": {
515
  "disease_name": "Early Blight",
516
  "common_names": ["Potato Early Blight"],
517
  "crop": "Potato",
@@ -536,7 +536,7 @@
536
  "type": "Fungal",
537
  "external_resources": []
538
  },
539
- "Potato___Late_blight": {
540
  "disease_name": "Late Blight",
541
  "common_names": ["Potato Late Blight"],
542
  "crop": "Potato",
@@ -565,7 +565,7 @@
565
  "type": "Fungal‑like",
566
  "external_resources": []
567
  },
568
- "Potato___healthy": {
569
  "disease_name": null,
570
  "common_names": [],
571
  "crop": "Potato",
@@ -584,7 +584,7 @@
584
  "type": "None",
585
  "external_resources": []
586
  },
587
- "Raspberry___healthy": {
588
  "disease_name": null,
589
  "common_names": [],
590
  "crop": "Raspberry",
@@ -605,7 +605,7 @@
605
  "type": "None",
606
  "external_resources": []
607
  },
608
- "Soybean___healthy": {
609
  "disease_name": null,
610
  "common_names": [],
611
  "crop": "Soybean",
@@ -624,7 +624,7 @@
624
  "type": "None",
625
  "external_resources": []
626
  },
627
- "Squash___Powdery_mildew": {
628
  "disease_name": "Powdery Mildew",
629
  "common_names": ["Squash powdery mildew"],
630
  "crop": "Squash",
@@ -652,7 +652,7 @@
652
  "type": "Fungal",
653
  "external_resources": []
654
  },
655
- "Strawberry___Leaf_scorch": {
656
  "disease_name": "Leaf Scorch",
657
  "common_names": ["Strawberry leaf scorch"],
658
  "crop": "Strawberry",
@@ -680,7 +680,7 @@
680
  "type": "Fungal",
681
  "external_resources": []
682
  },
683
- "Strawberry___healthy": {
684
  "disease_name": null,
685
  "common_names": [],
686
  "crop": "Strawberry",
@@ -701,7 +701,7 @@
701
  "type": "None",
702
  "external_resources": []
703
  },
704
- "Tomato___Bacterial_spot": {
705
  "disease_name": "Bacterial Spot",
706
  "common_names": ["Tomato Bacterial Spot"],
707
  "crop": "Tomato",
@@ -729,7 +729,7 @@
729
  "type": "Bacterial",
730
  "external_resources": []
731
  },
732
- "Tomato___Early_blight": {
733
  "disease_name": "Early Blight",
734
  "common_names": ["Tomato Early Blight"],
735
  "crop": "Tomato",
@@ -752,7 +752,7 @@
752
  "type": "Fungal",
753
  "external_resources": []
754
  },
755
- "Tomato___Leaf_Mold": {
756
  "disease_name": "Leaf Mold",
757
  "common_names": ["Tomato Leaf Mold"],
758
  "crop": "Tomato",
@@ -777,7 +777,7 @@
777
  "type": "Fungal",
778
  "external_resources": []
779
  },
780
- "Tomato___Septoria_leaf_spot": {
781
  "disease_name": "Septoria Leaf Spot",
782
  "common_names": ["Tomato Septoria Leaf Spot"],
783
  "crop": "Tomato",
@@ -802,7 +802,7 @@
802
  "type": "Fungal",
803
  "external_resources": []
804
  },
805
- "Tomato___Spider_mites_Two-spotted_spider_mite": {
806
  "disease_name": "Two‑Spotted Spider Mite Infestation",
807
  "common_names": ["Spider Mites", "Two‑Spotted Mite"],
808
  "crop": "Tomato",
@@ -827,7 +827,7 @@
827
  "type": "Pest",
828
  "external_resources": []
829
  },
830
- "Tomato___Target_Spot": {
831
  "disease_name": "Target Spot",
832
  "common_names": ["Tomato Target Spot"],
833
  "crop": "Tomato",
@@ -852,7 +852,7 @@
852
  "type": "Fungal",
853
  "external_resources": []
854
  },
855
- "Tomato___Tomato_Yellow_Leaf_Curl_Virus": {
856
  "disease_name": "Tomato Yellow Leaf Curl Virus",
857
  "common_names": ["TYLCV"],
858
  "crop": "Tomato",
@@ -877,7 +877,7 @@
877
  "type": "Viral",
878
  "external_resources": []
879
  },
880
- "Tomato___Tomato_mosaic_virus": {
881
  "disease_name": "Tomato Mosaic Virus",
882
  "common_names": ["ToMV"],
883
  "crop": "Tomato",
@@ -902,7 +902,7 @@
902
  "type": "Viral",
903
  "external_resources": []
904
  },
905
- "Tomato___healthy": {
906
  "disease_name": null,
907
  "common_names": [],
908
  "crop": "Tomato",
 
1
  {
2
+ "0": {
3
  "disease_name": "Apple Scab",
4
  "common_names": ["Apple scab"],
5
  "crop": "Apple",
 
34
  }
35
  ]
36
  },
37
+ "1": {
38
  "disease_name": "Black Rot",
39
  "common_names": ["Apple black rot"],
40
  "crop": "Apple",
 
60
  "type": "Fungal",
61
  "external_resources": []
62
  },
63
+ "2": {
64
  "disease_name": "Cedar Apple Rust",
65
  "common_names": ["Cedar rust"],
66
  "crop": "Apple",
 
86
  "type": "Fungal",
87
  "external_resources": []
88
  },
89
+ "3": {
90
  "disease_name": null,
91
  "common_names": [],
92
  "crop": "Apple",
 
105
  "type": "None",
106
  "external_resources": []
107
  },
108
+ "37": {
109
  "disease_name": "Late Blight",
110
  "common_names": ["Tomato late blight"],
111
  "crop": "Tomato",
 
130
  "type": "Fungal",
131
  "external_resources": []
132
  },
133
+ "4": {
134
  "disease_name": null,
135
  "common_names": [],
136
  "crop": "Blueberry",
 
149
  "type": "None",
150
  "external_resources": []
151
  },
152
+ "5": {
153
  "disease_name": "Powdery Mildew",
154
  "common_names": ["Cherry powdery mildew"],
155
  "crop": "Cherry (including sour)",
 
178
  "type": "Fungal",
179
  "external_resources": []
180
  },
181
+ "6": {
182
  "disease_name": null,
183
  "common_names": [],
184
  "crop": "Cherry (including sour)",
 
197
  "type": "None",
198
  "external_resources": []
199
  },
200
+ "7": {
201
  "disease_name": "Gray Leaf Spot",
202
  "common_names": ["Cercospora Leaf Spot", "Gray Leaf Spot"],
203
  "crop": "Corn (maize)",
 
225
  "type": "Fungal",
226
  "external_resources": []
227
  },
228
+ "8": {
229
  "disease_name": "Common Rust",
230
  "common_names": ["Corn Rust"],
231
  "crop": "Corn (maize)",
 
250
  "type": "Fungal",
251
  "external_resources": []
252
  },
253
+ "9": {
254
  "disease_name": "Northern Leaf Blight",
255
  "common_names": ["NLB"],
256
  "crop": "Corn (maize)",
 
275
  "type": "Fungal",
276
  "external_resources": []
277
  },
278
+ "10": {
279
  "disease_name": null,
280
  "common_names": [],
281
  "crop": "Corn (maize)",
 
294
  "type": "None",
295
  "external_resources": []
296
  },
297
+ "11": {
298
  "disease_name": "Black Rot",
299
  "common_names": ["Grape Black Rot"],
300
  "crop": "Grape",
 
319
  "type": "Fungal",
320
  "external_resources": []
321
  },
322
+ "12": {
323
  "disease_name": "Esca (Black Measles)",
324
  "common_names": ["Esca", "Black Measles"],
325
  "crop": "Grape",
 
342
  "type": "Fungal",
343
  "external_resources": []
344
  },
345
+ "13": {
346
  "disease_name": "Leaf Blight (Isariopsis Leaf Spot)",
347
  "common_names": ["Isariopsis Leaf Spot"],
348
  "crop": "Grape",
 
367
  "type": "Fungal",
368
  "external_resources": []
369
  },
370
+ "14": {
371
  "disease_name": null,
372
  "common_names": [],
373
  "crop": "Grape",
 
386
  "type": "None",
387
  "external_resources": []
388
  },
389
+ "15": {
390
  "disease_name": "Huanglongbing (Citrus Greening)",
391
  "common_names": ["Citrus Greening", "HLB"],
392
  "crop": "Orange",
 
417
  }
418
  ]
419
  },
420
+ "16": {
421
  "disease_name": "Bacterial Spot",
422
  "common_names": ["Peach Bacterial Spot"],
423
  "crop": "Peach",
 
443
  "type": "Bacterial",
444
  "external_resources": []
445
  },
446
+ "17": {
447
  "disease_name": null,
448
  "common_names": [],
449
  "crop": "Peach",
 
462
  "type": "None",
463
  "external_resources": []
464
  },
465
+ "18": {
466
  "disease_name": "Bacterial Spot",
467
  "common_names": ["Bell Pepper Bacterial Spot"],
468
  "crop": "Pepper, bell",
 
490
  "type": "Bacterial",
491
  "external_resources": []
492
  },
493
+ "19": {
494
  "disease_name": null,
495
  "common_names": [],
496
  "crop": "Pepper, bell",
 
511
  "type": "None",
512
  "external_resources": []
513
  },
514
+ "20": {
515
  "disease_name": "Early Blight",
516
  "common_names": ["Potato Early Blight"],
517
  "crop": "Potato",
 
536
  "type": "Fungal",
537
  "external_resources": []
538
  },
539
+ "21": {
540
  "disease_name": "Late Blight",
541
  "common_names": ["Potato Late Blight"],
542
  "crop": "Potato",
 
565
  "type": "Fungal‑like",
566
  "external_resources": []
567
  },
568
+ "22": {
569
  "disease_name": null,
570
  "common_names": [],
571
  "crop": "Potato",
 
584
  "type": "None",
585
  "external_resources": []
586
  },
587
+ "23": {
588
  "disease_name": null,
589
  "common_names": [],
590
  "crop": "Raspberry",
 
605
  "type": "None",
606
  "external_resources": []
607
  },
608
+ "24": {
609
  "disease_name": null,
610
  "common_names": [],
611
  "crop": "Soybean",
 
624
  "type": "None",
625
  "external_resources": []
626
  },
627
+ "25": {
628
  "disease_name": "Powdery Mildew",
629
  "common_names": ["Squash powdery mildew"],
630
  "crop": "Squash",
 
652
  "type": "Fungal",
653
  "external_resources": []
654
  },
655
+ "26": {
656
  "disease_name": "Leaf Scorch",
657
  "common_names": ["Strawberry leaf scorch"],
658
  "crop": "Strawberry",
 
680
  "type": "Fungal",
681
  "external_resources": []
682
  },
683
+ "27": {
684
  "disease_name": null,
685
  "common_names": [],
686
  "crop": "Strawberry",
 
701
  "type": "None",
702
  "external_resources": []
703
  },
704
+ "28": {
705
  "disease_name": "Bacterial Spot",
706
  "common_names": ["Tomato Bacterial Spot"],
707
  "crop": "Tomato",
 
729
  "type": "Bacterial",
730
  "external_resources": []
731
  },
732
+ "29": {
733
  "disease_name": "Early Blight",
734
  "common_names": ["Tomato Early Blight"],
735
  "crop": "Tomato",
 
752
  "type": "Fungal",
753
  "external_resources": []
754
  },
755
+ "30": {
756
  "disease_name": "Leaf Mold",
757
  "common_names": ["Tomato Leaf Mold"],
758
  "crop": "Tomato",
 
777
  "type": "Fungal",
778
  "external_resources": []
779
  },
780
+ "31": {
781
  "disease_name": "Septoria Leaf Spot",
782
  "common_names": ["Tomato Septoria Leaf Spot"],
783
  "crop": "Tomato",
 
802
  "type": "Fungal",
803
  "external_resources": []
804
  },
805
+ "32": {
806
  "disease_name": "Two‑Spotted Spider Mite Infestation",
807
  "common_names": ["Spider Mites", "Two‑Spotted Mite"],
808
  "crop": "Tomato",
 
827
  "type": "Pest",
828
  "external_resources": []
829
  },
830
+ "33": {
831
  "disease_name": "Target Spot",
832
  "common_names": ["Tomato Target Spot"],
833
  "crop": "Tomato",
 
852
  "type": "Fungal",
853
  "external_resources": []
854
  },
855
+ "34": {
856
  "disease_name": "Tomato Yellow Leaf Curl Virus",
857
  "common_names": ["TYLCV"],
858
  "crop": "Tomato",
 
877
  "type": "Viral",
878
  "external_resources": []
879
  },
880
+ "35": {
881
  "disease_name": "Tomato Mosaic Virus",
882
  "common_names": ["ToMV"],
883
  "crop": "Tomato",
 
902
  "type": "Viral",
903
  "external_resources": []
904
  },
905
+ "36": {
906
  "disease_name": null,
907
  "common_names": [],
908
  "crop": "Tomato",
main.py CHANGED
@@ -2,17 +2,19 @@ import json
2
  import os
3
  import logging
4
  from typing import Dict, Optional, Any, List
 
5
  from pydantic import BaseModel, Field, field_validator
6
  from contextlib import asynccontextmanager
7
  from rapidfuzz import process, fuzz
8
  import urllib.parse
9
-
10
- from fastapi.responses import JSONResponse
11
  from fastapi.middleware.cors import CORSMiddleware
 
12
 
13
  import numpy as np
14
  import tensorflow as tf
15
- from fastapi import FastAPI, File, Path, Query, UploadFile, HTTPException, status
16
  from PIL import Image
17
  import io
18
  from huggingface_hub import hf_hub_download
@@ -30,8 +32,8 @@ IMAGE_SIZE: tuple = (300, 300)
30
  MAX_FILE_SIZE_MB: int = 10
31
  CONFIDENCE_THRESHOLD: float = 0.5
32
 
33
- # Plant disease class names
34
- CLASS_NAMES = ['Apple___Apple_scab', 'Apple___Black_rot', 'Apple___Cedar_apple_rust', 'Apple___healthy', 'Blueberry___healthy', 'Cherry_(including_sour)___Powdery_mildew', 'Cherry_(including_sour)___healthy', 'Corn_(maize)___Cercospora_leaf_spot Gray_leaf_spot', 'Corn_(maize)___Common_rust_', 'Corn_(maize)___Northern_Leaf_Blight', 'Corn_(maize)___healthy', 'Grape___Black_rot', 'Grape___Esca_(Black_Measles)', 'Grape___Leaf_blight_(Isariopsis_Leaf_Spot)', 'Grape___healthy', 'Orange___Haunglongbing_(Citrus_greening)', 'Peach___Bacterial_spot', 'Peach___healthy', 'Pepper,_bell___Bacterial_spot', 'Pepper,_bell___healthy', 'Potato___Early_blight', 'Potato___Late_blight', 'Potato___healthy', 'Raspberry___healthy', 'Soybean___healthy', 'Squash___Powdery_mildew', 'Strawberry___Leaf_scorch', 'Strawberry___healthy', 'Tomato___Bacterial_spot', 'Tomato___Early_blight', 'Tomato___Late_blight', 'Tomato___Leaf_Mold', 'Tomato___Septoria_leaf_spot', 'Tomato___Spider_mites Two-spotted_spider_mite', 'Tomato___Target_Spot', 'Tomato___Tomato_Yellow_Leaf_Curl_Virus', 'Tomato___Tomato_mosaic_virus', 'Tomato___healthy']
35
 
36
  # HTTP Status Messages
37
  HTTP_MESSAGES = {
@@ -49,6 +51,7 @@ HTTP_MESSAGES = {
49
  model: Optional[tf.keras.Model] = None
50
  disease_guide: Dict[str, Dict[str, Any]] = {}
51
 
 
52
  # Response models with improved validation
53
 
54
  class DiseaseInfo(BaseModel):
@@ -67,6 +70,7 @@ class DiseaseInfo(BaseModel):
67
  localized_tips: str = ""
68
  type: str = "Unknown"
69
  external_resources: List[Dict[str, str]] = []
 
70
 
71
  @field_validator('external_resources', mode='before')
72
  @classmethod
@@ -95,13 +99,16 @@ class DiseaseInfo(BaseModel):
95
  elif field_name in ['common_names', 'symptoms', 'treatment', 'image_urls', 'prevention', 'external_resources']:
96
  return []
97
  elif field_name in ['crop', 'description', 'management_tips', 'risk_level', 'sprayer_intervals', 'localized_tips', 'type']:
98
- return info.default
 
 
99
  return v
100
 
101
 
102
  class PredictionResponse(BaseModel):
103
  success: bool
104
  predicted_class: str
 
105
  clean_class_name: str = Field(description="Human-readable class name")
106
  confidence: float
107
  confidence_level: str = Field(description="High/Medium/Low confidence level")
@@ -116,6 +123,7 @@ class HealthResponse(BaseModel):
116
  model_loaded: bool
117
  total_classes: int
118
  available_diseases: int
 
119
  message: str
120
 
121
  class SearchResult(BaseModel):
@@ -155,14 +163,16 @@ def load_disease_guide() -> Dict[str, Dict[str, Any]]:
155
  logger.error(f"Failed to load disease guide: {str(e)}")
156
  return {}
157
 
158
- def clean_class_name(class_name: str) -> str:
159
- """Convert class name to human-readable format"""
160
- # Replace underscores with spaces and clean up formatting
161
- cleaned = class_name.replace('___', ' - ').replace('_', ' ')
162
- # Handle special cases
163
- cleaned = cleaned.replace('(including sour)', '(including sour)')
164
- cleaned = cleaned.replace('Two-spotted spider mite', 'Two-spotted spider mite')
165
- return cleaned.title()
 
 
166
 
167
  def get_confidence_level(confidence: float) -> str:
168
  """Categorize confidence level"""
@@ -172,10 +182,24 @@ def get_confidence_level(confidence: float) -> str:
172
  return "Medium"
173
  else:
174
  return "Low"
 
 
 
 
175
 
176
- def safe_create_disease_info(disease_data: Dict[str, Any]) -> DiseaseInfo:
177
  """Safely create DiseaseInfo object with proper validation and defaults"""
178
  try:
 
 
 
 
 
 
 
 
 
 
179
  # Create a copy of the data to avoid modifying the original
180
  safe_data = disease_data.copy()
181
 
@@ -195,7 +219,8 @@ def safe_create_disease_info(disease_data: Dict[str, Any]) -> DiseaseInfo:
195
  'sprayer_intervals': safe_data.get('sprayer_intervals', ''),
196
  'localized_tips': safe_data.get('localized_tips', ''),
197
  'type': safe_data.get('type', 'Unknown'),
198
- 'external_resources': safe_data.get('external_resources', [])
 
199
  }
200
 
201
  # Validate and clean external_resources
@@ -212,51 +237,90 @@ def safe_create_disease_info(disease_data: Dict[str, Any]) -> DiseaseInfo:
212
  return DiseaseInfo(**defaults)
213
 
214
  except Exception as e:
215
- logger.error(f"Error creating DiseaseInfo: {str(e)}")
216
  logger.error(f"Data causing error: {disease_data}")
217
 
218
  # Return a minimal valid DiseaseInfo object
219
  return DiseaseInfo(
220
  disease_name="Unknown",
221
  crop="Unknown",
222
- description="Error loading disease information",
223
- cause="Unknown"
 
224
  )
225
 
226
- def get_recommendations(predicted_class: str, confidence: float, disease_info: DiseaseInfo) -> List[str]:
227
- """Generate actionable recommendations based on prediction"""
228
  recommendations = []
229
 
 
230
  if confidence < CONFIDENCE_THRESHOLD:
231
- recommendations.append("Consider taking a clearer, well-lit photo for better accuracy")
232
- recommendations.append("Ensure the leaf fills most of the frame")
 
 
 
233
 
234
- if disease_info.disease_name and disease_info.disease_name != "Unknown":
235
- # Disease detected
236
- treatments = disease_info.treatment if disease_info.treatment else ["Consult agricultural expert"]
237
  recommendations.extend([
238
- f"Immediate action: {treatments[0]}",
239
- "Isolate affected plants to prevent spread",
240
- "Monitor other plants for similar symptoms"
 
 
 
241
  ])
242
-
 
243
  if disease_info.risk_level == "High":
244
- recommendations.insert(0, "⚠️ HIGH RISK: Take immediate action to prevent crop loss")
245
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  # Add management tips if available
247
  if disease_info.management_tips:
248
- recommendations.append(f"Management tip: {disease_info.management_tips}")
249
 
250
  # Add sprayer intervals if available
251
  if disease_info.sprayer_intervals:
252
- recommendations.append(f"Spraying schedule: {disease_info.sprayer_intervals}")
253
- else:
254
- # Healthy plant
 
 
 
 
255
  recommendations.extend([
256
- "Plant appears healthy - continue current care routine",
257
- "Monitor regularly for any changes",
258
- "Maintain preventive measures"
 
259
  ])
 
 
 
 
 
 
 
260
 
261
  return recommendations
262
 
@@ -352,35 +416,43 @@ def predict_image(image_bytes: bytes) -> PredictionResponse:
352
  predicted_class_idx = np.argmax(predictions[0])
353
  confidence = float(predictions[0][predicted_class_idx])
354
 
355
- # Get predicted class
356
- predicted_class = CLASS_NAMES[predicted_class_idx]
357
- clean_name = clean_class_name(predicted_class)
 
 
 
 
 
 
358
  confidence_level = get_confidence_level(confidence)
359
  class_id = create_class_id(predicted_class)
360
 
361
- # Get top 5 predictions
362
  top_indices = np.argsort(predictions[0])[-5:][::-1]
363
- all_predictions = {
364
- clean_class_name(CLASS_NAMES[idx]): float(predictions[0][idx])
365
- for idx in top_indices
366
- }
367
 
368
- # Get disease information with safe creation
369
- disease_data = disease_guide.get(predicted_class, {})
370
- disease_info = safe_create_disease_info(disease_data)
 
 
371
 
372
- # Generate recommendations
373
  recommendations = get_recommendations(predicted_class, confidence, disease_info)
374
 
375
- # Determine message based on confidence
376
  if confidence < CONFIDENCE_THRESHOLD:
377
  message = HTTP_MESSAGES["LOW_CONFIDENCE"]
 
 
378
  else:
379
- message = "Prediction completed successfully"
380
 
381
  return PredictionResponse(
382
  success=True,
383
  predicted_class=predicted_class,
 
384
  clean_class_name=clean_name,
385
  confidence=confidence,
386
  confidence_level=confidence_level,
@@ -407,6 +479,7 @@ def is_image_file(filename: str) -> bool:
407
  image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
408
  return any(filename.lower().endswith(ext) for ext in image_extensions)
409
 
 
410
  @asynccontextmanager
411
  async def lifespan(app: FastAPI):
412
  """Handle startup and shutdown events"""
@@ -439,7 +512,7 @@ async def lifespan(app: FastAPI):
439
  app = FastAPI(
440
  title="Plant Disease Prediction API",
441
  description="API for predicting plant diseases from leaf images using deep learning",
442
- version="2.1.0",
443
  lifespan=lifespan,
444
  docs_url="/docs",
445
  redoc_url="/redoc"
@@ -454,29 +527,51 @@ app.add_middleware(
454
  allow_headers=["*"],
455
  )
456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  @app.get("/", response_model=HealthResponse)
458
  async def root():
459
  """Root endpoint with API information"""
 
 
 
460
  return HealthResponse(
461
  status="running",
462
  model_loaded=model is not None,
463
  total_classes=len(CLASS_NAMES),
464
- available_diseases=len([d for d in disease_guide.values() if d.get("disease_name")]),
 
465
  message="Plant Disease Prediction API is running"
466
  )
467
 
468
  @app.get("/health", response_model=HealthResponse)
469
  async def health_check():
470
  """Health check endpoint"""
 
 
 
471
  return HealthResponse(
472
  status="healthy" if model is not None else "unhealthy",
473
  model_loaded=model is not None,
474
  total_classes=len(CLASS_NAMES),
475
- available_diseases=len([d for d in disease_guide.values() if d.get("disease_name")]),
 
476
  message=HTTP_MESSAGES["MODEL_LOAD_SUCCESS"] if model is not None else HTTP_MESSAGES["MODEL_NOT_LOADED"]
477
  )
478
 
479
  @app.post("/predict", response_model=PredictionResponse)
 
480
  async def predict_plant_disease(file: UploadFile = File(...)):
481
  """
482
  Predict plant disease from uploaded image
@@ -515,12 +610,17 @@ async def predict_plant_disease(file: UploadFile = File(...)):
515
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
516
  detail=HTTP_MESSAGES["IMAGE_PROCESSING_FAILED"].format(error=str(e))
517
  )
 
 
 
 
518
 
519
  @app.get("/diseases", response_model=List[SearchResult])
520
  async def get_all_plant_diseases(
521
  crop: Optional[str] = Query(None, description="Filter by crop name (e.g. Apple, Tomato)"),
522
  disease_type: Optional[str] = Query(None, description="Filter by disease type (Fungal, Bacterial, Viral)"),
523
- risk_level: Optional[str] = Query(None, description="Filter by risk level (High, Medium, Low)")
 
524
  ):
525
  """
526
  Get all plant diseases with optional filtering
@@ -528,21 +628,23 @@ async def get_all_plant_diseases(
528
  diseases = []
529
 
530
  for class_name, info in disease_guide.items():
531
- if not info.get("disease_name"):
532
- continue # Skip healthy entries
533
-
534
- # Apply filters
535
- if crop and info.get("crop", "").lower() != crop.lower():
536
- continue
537
- if disease_type and info.get("type", "").lower() != disease_type.lower():
538
- continue
539
- if risk_level and info.get("risk_level", "").lower() != risk_level.lower():
540
  continue
541
 
 
 
 
 
 
 
 
 
 
542
  diseases.append(SearchResult(
543
  class_name=class_name,
544
  class_id=create_class_id(class_name),
545
- disease_info=safe_create_disease_info(info)
546
  ))
547
 
548
  return diseases
@@ -550,36 +652,54 @@ async def get_all_plant_diseases(
550
  @app.get("/search", response_model=SearchResponse)
551
  async def search_diseases(
552
  query: str = Query(..., min_length=1, description="Search term"),
553
- limit: int = Query(10, ge=1, le=50, description="Maximum number of results")
 
554
  ):
555
  """
556
  Search plant diseases with fuzzy matching and relevance scoring
557
  """
558
- query_lower = query.lower()
 
 
 
 
 
 
 
 
 
 
 
 
559
  exact_matches = []
560
  fuzzy_candidates = []
561
 
562
  for class_name, info in disease_guide.items():
563
- if not info.get("disease_name"):
 
564
  continue
565
 
566
  # Build searchable text
567
- searchable_text = " ".join([
568
- class_name,
569
- info.get("disease_name", ""),
570
- info.get("description", ""),
571
- info.get("crop", ""),
572
- info.get("type", ""),
573
- " ".join(info.get("symptoms", [])),
574
- " ".join(info.get("common_names", []))
575
- ]).lower()
 
 
 
 
576
 
577
  # Check for exact substring matches
578
  if query_lower in searchable_text:
579
  exact_matches.append(SearchResult(
580
  class_name=class_name,
581
  class_id=create_class_id(class_name),
582
- disease_info=safe_create_disease_info(info)
583
  ))
584
  else:
585
  fuzzy_candidates.append((class_name, info, searchable_text))
@@ -594,26 +714,34 @@ async def search_diseases(
594
 
595
  # Fuzzy search on candidates
596
  search_texts = [text for _, _, text in fuzzy_candidates]
597
- fuzzy_matches = process.extract(
598
- query, search_texts, scorer=fuzz.token_sort_ratio, limit=limit
599
- )
600
-
601
- suggestions = []
602
- for match_text, score, idx in fuzzy_matches:
603
- if score > 60: # Minimum relevance threshold
604
- class_name, info, _ = fuzzy_candidates[idx]
605
- suggestions.append(SearchResult(
606
- class_name=class_name,
607
- class_id=create_class_id(class_name),
608
- disease_info=safe_create_disease_info(info),
609
- relevance_score=score
610
- ))
 
 
 
 
 
 
 
 
611
 
612
  return SearchResponse(
613
  results=[],
614
- suggestions=suggestions,
615
- total_results=len(suggestions),
616
- message="No exact matches found. Showing relevant suggestions." if suggestions else "No matches found."
617
  )
618
 
619
  @app.get("/diseases/{class_id}", response_model=SearchResult)
@@ -627,18 +755,19 @@ async def get_disease_by_class_id(
627
  # Decode the class_id back to class_name
628
  class_name = decode_class_id(class_id)
629
 
630
- disease_data = disease_guide.get(class_name)
631
-
632
- if not disease_data:
633
  raise HTTPException(
634
  status_code=status.HTTP_404_NOT_FOUND,
635
- detail=f"Disease with class ID '{class_id}' not found."
636
  )
637
 
 
 
638
  return SearchResult(
639
  class_name=class_name,
640
  class_id=class_id,
641
- disease_info=safe_create_disease_info(disease_data)
642
  )
643
 
644
  except UnicodeDecodeError:
@@ -649,24 +778,24 @@ async def get_disease_by_class_id(
649
 
650
  @app.get("/diseases/by-name/{class_name}", response_model=SearchResult)
651
  async def get_disease_by_class_name(
652
- class_name: str = Path(..., description="Exact class name, e.g. Apple___Apple_scab")
653
  ):
654
  """
655
  Retrieve detailed information for a specific disease class by exact class name
656
  (Alternative endpoint for direct class name access)
657
  """
658
- disease_data = disease_guide.get(class_name)
659
-
660
- if not disease_data:
661
  raise HTTPException(
662
  status_code=status.HTTP_404_NOT_FOUND,
663
- detail=f"Disease with class name '{class_name}' not found."
664
  )
665
 
 
666
  return SearchResult(
667
  class_name=class_name,
668
  class_id=create_class_id(class_name),
669
- disease_info=safe_create_disease_info(disease_data)
670
  )
671
 
672
  @app.get("/stats")
 
2
  import os
3
  import logging
4
  from typing import Dict, Optional, Any, List
5
+ import uuid
6
  from pydantic import BaseModel, Field, field_validator
7
  from contextlib import asynccontextmanager
8
  from rapidfuzz import process, fuzz
9
  import urllib.parse
10
+ from slowapi import Limiter
11
+ from slowapi.util import get_remote_address
12
  from fastapi.middleware.cors import CORSMiddleware
13
+ from bleach import clean
14
 
15
  import numpy as np
16
  import tensorflow as tf
17
+ from fastapi import FastAPI, File, Path, Query, Request, UploadFile, HTTPException, status
18
  from PIL import Image
19
  import io
20
  from huggingface_hub import hf_hub_download
 
32
  MAX_FILE_SIZE_MB: int = 10
33
  CONFIDENCE_THRESHOLD: float = 0.5
34
 
35
+ # Plant disease class names - these are the actual class indices that the model outputs
36
+ CLASS_NAMES = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "36", "37"]
37
 
38
  # HTTP Status Messages
39
  HTTP_MESSAGES = {
 
51
  model: Optional[tf.keras.Model] = None
52
  disease_guide: Dict[str, Dict[str, Any]] = {}
53
 
54
+
55
  # Response models with improved validation
56
 
57
  class DiseaseInfo(BaseModel):
 
70
  localized_tips: str = ""
71
  type: str = "Unknown"
72
  external_resources: List[Dict[str, str]] = []
73
+ is_healthy: bool = False
74
 
75
  @field_validator('external_resources', mode='before')
76
  @classmethod
 
99
  elif field_name in ['common_names', 'symptoms', 'treatment', 'image_urls', 'prevention', 'external_resources']:
100
  return []
101
  elif field_name in ['crop', 'description', 'management_tips', 'risk_level', 'sprayer_intervals', 'localized_tips', 'type']:
102
+ return info.default if hasattr(info, 'default') else "Unknown"
103
+ elif field_name == 'is_healthy':
104
+ return False
105
  return v
106
 
107
 
108
  class PredictionResponse(BaseModel):
109
  success: bool
110
  predicted_class: str
111
+ predicted_class_index: int
112
  clean_class_name: str = Field(description="Human-readable class name")
113
  confidence: float
114
  confidence_level: str = Field(description="High/Medium/Low confidence level")
 
123
  model_loaded: bool
124
  total_classes: int
125
  available_diseases: int
126
+ healthy_classes: int
127
  message: str
128
 
129
  class SearchResult(BaseModel):
 
163
  logger.error(f"Failed to load disease guide: {str(e)}")
164
  return {}
165
 
166
+ def clean_class_name(class_index: str, disease_info: Optional[Dict[str, Any]] = None) -> str:
167
+ """Convert class index to human-readable format"""
168
+ if disease_info and disease_info.get('disease_name'):
169
+ # Use the disease name from the JSON if available
170
+ disease_name = disease_info['disease_name']
171
+ crop = disease_info.get('crop', 'Unknown')
172
+ return f"{crop} - {disease_name}"
173
+ else:
174
+ # For healthy plants or unknown diseases
175
+ return f"Class {class_index} (Healthy/Unknown)"
176
 
177
  def get_confidence_level(confidence: float) -> str:
178
  """Categorize confidence level"""
 
182
  return "Medium"
183
  else:
184
  return "Low"
185
+
186
+ def sanitize_search_query(query: str) -> str:
187
+ """Sanitize search input"""
188
+ return clean(query.strip(), tags=[], strip=True)[:100] # Limit length
189
 
190
+ def safe_create_disease_info(class_index: str, disease_data: Optional[Dict[str, Any]] = None) -> DiseaseInfo:
191
  """Safely create DiseaseInfo object with proper validation and defaults"""
192
  try:
193
+ if not disease_data:
194
+ # This is likely a healthy plant or unknown disease
195
+ return DiseaseInfo(
196
+ disease_name=None,
197
+ crop="Unknown",
198
+ description=f"This appears to be a healthy plant or an unrecognized condition for class {class_index}",
199
+ is_healthy=True,
200
+ type="Healthy/Unknown"
201
+ )
202
+
203
  # Create a copy of the data to avoid modifying the original
204
  safe_data = disease_data.copy()
205
 
 
219
  'sprayer_intervals': safe_data.get('sprayer_intervals', ''),
220
  'localized_tips': safe_data.get('localized_tips', ''),
221
  'type': safe_data.get('type', 'Unknown'),
222
+ 'external_resources': safe_data.get('external_resources', []),
223
+ 'is_healthy': False
224
  }
225
 
226
  # Validate and clean external_resources
 
237
  return DiseaseInfo(**defaults)
238
 
239
  except Exception as e:
240
+ logger.error(f"Error creating DiseaseInfo for class {class_index}: {str(e)}")
241
  logger.error(f"Data causing error: {disease_data}")
242
 
243
  # Return a minimal valid DiseaseInfo object
244
  return DiseaseInfo(
245
  disease_name="Unknown",
246
  crop="Unknown",
247
+ description=f"Error loading disease information for class {class_index}",
248
+ cause="Unknown",
249
+ is_healthy=False
250
  )
251
 
252
+ def get_recommendations(class_index: str, confidence: float, disease_info: DiseaseInfo) -> List[str]:
253
+ """Generate actionable recommendations based on prediction using treatment and prevention from JSON"""
254
  recommendations = []
255
 
256
+ # Add confidence-based recommendations first
257
  if confidence < CONFIDENCE_THRESHOLD:
258
+ recommendations.extend([
259
+ "⚠️ Low confidence prediction - consider taking a clearer, well-lit photo",
260
+ "📸 Ensure the leaf/plant fills most of the frame and is in focus",
261
+ "💡 Try taking photos in natural light for better results"
262
+ ])
263
 
264
+ if disease_info.is_healthy or not disease_info.disease_name:
265
+ # Healthy plant recommendations
 
266
  recommendations.extend([
267
+ " Plant appears healthy - continue current care routine",
268
+ "👀 Monitor regularly for any changes in leaf color, spots, or wilting",
269
+ "💧 Maintain proper watering schedule - avoid overwatering",
270
+ "🌱 Ensure adequate fertilization and soil drainage",
271
+ "🛡️ Consider preventive measures during disease-prone seasons",
272
+ "🌿 Keep the growing area clean and remove fallen debris"
273
  ])
274
+ else:
275
+ # Disease detected - use treatment and prevention from JSON
276
  if disease_info.risk_level == "High":
277
+ recommendations.insert(0, "🚨 HIGH RISK DISEASE: Take immediate action to prevent crop loss")
278
+ elif disease_info.risk_level == "Medium":
279
+ recommendations.insert(0, "⚠️ MEDIUM RISK DISEASE: Prompt treatment recommended")
280
+
281
+ # Add disease identification
282
+ recommendations.append(f"🔬 Disease identified: {disease_info.disease_name}")
283
+
284
+ # Add treatments from JSON
285
+ if disease_info.treatment:
286
+ recommendations.append("💊 **TREATMENT RECOMMENDATIONS:**")
287
+ for i, treatment in enumerate(disease_info.treatment, 1):
288
+ recommendations.append(f" {i}. {treatment}")
289
+ else:
290
+ recommendations.append("💊 Consult agricultural expert for proper treatment")
291
+
292
+ # Add prevention measures from JSON
293
+ if disease_info.prevention:
294
+ recommendations.append("🛡️ **PREVENTION MEASURES:**")
295
+ for i, prevention in enumerate(disease_info.prevention, 1):
296
+ recommendations.append(f" {i}. {prevention}")
297
+
298
  # Add management tips if available
299
  if disease_info.management_tips:
300
+ recommendations.append(f"💡 **MANAGEMENT TIP:** {disease_info.management_tips}")
301
 
302
  # Add sprayer intervals if available
303
  if disease_info.sprayer_intervals:
304
+ recommendations.append(f"🚿 **SPRAYING SCHEDULE:** {disease_info.sprayer_intervals}")
305
+
306
+ # Add localized tips if available
307
+ if disease_info.localized_tips:
308
+ recommendations.append(f"🎯 **LOCALIZED TIP:** {disease_info.localized_tips}")
309
+
310
+ # General disease management recommendations
311
  recommendations.extend([
312
+ "🔒 Isolate affected plants to prevent spread to healthy plants",
313
+ "👀 Monitor other plants regularly for similar symptoms",
314
+ "🗑️ Remove and destroy infected plant material properly",
315
+ "🧼 Sanitize tools and hands after handling infected plants"
316
  ])
317
+
318
+ # Add external resources if available
319
+ if disease_info.external_resources:
320
+ recommendations.append("📚 **ADDITIONAL RESOURCES:**")
321
+ for resource in disease_info.external_resources:
322
+ if resource.get('title') and resource.get('url'):
323
+ recommendations.append(f" • {resource['title']}: {resource['url']}")
324
 
325
  return recommendations
326
 
 
416
  predicted_class_idx = np.argmax(predictions[0])
417
  confidence = float(predictions[0][predicted_class_idx])
418
 
419
+ # Get predicted class as string (this matches the JSON keys)
420
+ predicted_class = str(predicted_class_idx)
421
+
422
+ # Get disease information from the JSON using the string key
423
+ disease_data = disease_guide.get(predicted_class, None)
424
+ disease_info = safe_create_disease_info(predicted_class, disease_data)
425
+
426
+ # Create clean class name
427
+ clean_name = clean_class_name(predicted_class, disease_data)
428
  confidence_level = get_confidence_level(confidence)
429
  class_id = create_class_id(predicted_class)
430
 
431
+ # Get top 5 predictions with their disease info
432
  top_indices = np.argsort(predictions[0])[-5:][::-1]
433
+ all_predictions = {}
 
 
 
434
 
435
+ for idx in top_indices:
436
+ class_str = str(idx)
437
+ class_disease_data = disease_guide.get(class_str, None)
438
+ readable_name = clean_class_name(class_str, class_disease_data)
439
+ all_predictions[readable_name] = float(predictions[0][idx])
440
 
441
+ # Generate recommendations using the updated function
442
  recommendations = get_recommendations(predicted_class, confidence, disease_info)
443
 
444
+ # Determine message based on confidence and disease status
445
  if confidence < CONFIDENCE_THRESHOLD:
446
  message = HTTP_MESSAGES["LOW_CONFIDENCE"]
447
+ elif disease_info.is_healthy:
448
+ message = "Plant appears healthy! Continue with regular care."
449
  else:
450
+ message = f"Disease detected: {disease_info.disease_name or 'Unknown condition'}"
451
 
452
  return PredictionResponse(
453
  success=True,
454
  predicted_class=predicted_class,
455
+ predicted_class_index=predicted_class_idx,
456
  clean_class_name=clean_name,
457
  confidence=confidence,
458
  confidence_level=confidence_level,
 
479
  image_extensions = {'.jpg', '.jpeg', '.png', '.bmp', '.gif', '.tiff', '.webp'}
480
  return any(filename.lower().endswith(ext) for ext in image_extensions)
481
 
482
+
483
  @asynccontextmanager
484
  async def lifespan(app: FastAPI):
485
  """Handle startup and shutdown events"""
 
512
  app = FastAPI(
513
  title="Plant Disease Prediction API",
514
  description="API for predicting plant diseases from leaf images using deep learning",
515
+ version="2.2.0",
516
  lifespan=lifespan,
517
  docs_url="/docs",
518
  redoc_url="/redoc"
 
527
  allow_headers=["*"],
528
  )
529
 
530
+ @app.middleware("http")
531
+ async def add_request_id(request: Request, call_next):
532
+ request_id = str(uuid.uuid4())
533
+ request.state.request_id = request_id
534
+
535
+ with logger.contextvars(request_id=request_id):
536
+ response = await call_next(request)
537
+
538
+ return response
539
+
540
+ limiter = Limiter(key_func=get_remote_address)
541
+ app.state.limiter = limiter
542
+
543
  @app.get("/", response_model=HealthResponse)
544
  async def root():
545
  """Root endpoint with API information"""
546
+ disease_count = len([d for d in disease_guide.values() if d.get("disease_name")])
547
+ healthy_count = len(CLASS_NAMES) - disease_count
548
+
549
  return HealthResponse(
550
  status="running",
551
  model_loaded=model is not None,
552
  total_classes=len(CLASS_NAMES),
553
+ available_diseases=disease_count,
554
+ healthy_classes=healthy_count,
555
  message="Plant Disease Prediction API is running"
556
  )
557
 
558
  @app.get("/health", response_model=HealthResponse)
559
  async def health_check():
560
  """Health check endpoint"""
561
+ disease_count = len([d for d in disease_guide.values() if d.get("disease_name")])
562
+ healthy_count = len(CLASS_NAMES) - disease_count
563
+
564
  return HealthResponse(
565
  status="healthy" if model is not None else "unhealthy",
566
  model_loaded=model is not None,
567
  total_classes=len(CLASS_NAMES),
568
+ available_diseases=disease_count,
569
+ healthy_classes=healthy_count,
570
  message=HTTP_MESSAGES["MODEL_LOAD_SUCCESS"] if model is not None else HTTP_MESSAGES["MODEL_NOT_LOADED"]
571
  )
572
 
573
  @app.post("/predict", response_model=PredictionResponse)
574
+ @limiter.limit("10/minute")
575
  async def predict_plant_disease(file: UploadFile = File(...)):
576
  """
577
  Predict plant disease from uploaded image
 
610
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
611
  detail=HTTP_MESSAGES["IMAGE_PROCESSING_FAILED"].format(error=str(e))
612
  )
613
+ finally:
614
+ # Explicit cleanup for large files
615
+ if image_bytes:
616
+ del image_bytes
617
 
618
  @app.get("/diseases", response_model=List[SearchResult])
619
  async def get_all_plant_diseases(
620
  crop: Optional[str] = Query(None, description="Filter by crop name (e.g. Apple, Tomato)"),
621
  disease_type: Optional[str] = Query(None, description="Filter by disease type (Fungal, Bacterial, Viral)"),
622
+ risk_level: Optional[str] = Query(None, description="Filter by risk level (High, Medium, Low)"),
623
+ include_healthy: bool = Query(False, description="Include healthy/unknown classes")
624
  ):
625
  """
626
  Get all plant diseases with optional filtering
 
628
  diseases = []
629
 
630
  for class_name, info in disease_guide.items():
631
+ # Skip healthy classes unless specifically requested
632
+ if not include_healthy and not info.get("disease_name"):
 
 
 
 
 
 
 
633
  continue
634
 
635
+ # Apply filters (only for disease entries)
636
+ if info.get("disease_name"): # Only apply filters to actual diseases
637
+ if crop and info.get("crop", "").lower() != crop.lower():
638
+ continue
639
+ if disease_type and info.get("type", "").lower() != disease_type.lower():
640
+ continue
641
+ if risk_level and info.get("risk_level", "").lower() != risk_level.lower():
642
+ continue
643
+
644
  diseases.append(SearchResult(
645
  class_name=class_name,
646
  class_id=create_class_id(class_name),
647
+ disease_info=safe_create_disease_info(class_name, info if info.get("disease_name") else None)
648
  ))
649
 
650
  return diseases
 
652
  @app.get("/search", response_model=SearchResponse)
653
  async def search_diseases(
654
  query: str = Query(..., min_length=1, description="Search term"),
655
+ limit: int = Query(10, ge=1, le=50, description="Maximum number of results"),
656
+ include_healthy: bool = Query(False, description="Include healthy/unknown classes in search")
657
  ):
658
  """
659
  Search plant diseases with fuzzy matching and relevance scoring
660
  """
661
+ cleaned_query = sanitize_search_query(query)
662
+ if not cleaned_query:
663
+ raise HTTPException(
664
+ status_code=status.HTTP_400_BAD_REQUEST,
665
+ detail="Search query cannot be empty"
666
+ )
667
+ if len(cleaned_query) < 2:
668
+ raise HTTPException(
669
+ status_code=status.HTTP_400_BAD_REQUEST,
670
+ detail="Search query must be at least 2 characters long"
671
+ )
672
+
673
+ query_lower = cleaned_query.lower()
674
  exact_matches = []
675
  fuzzy_candidates = []
676
 
677
  for class_name, info in disease_guide.items():
678
+ # Skip healthy classes unless specifically requested
679
+ if not include_healthy and not info.get("disease_name"):
680
  continue
681
 
682
  # Build searchable text
683
+ searchable_text_parts = [class_name]
684
+
685
+ if info.get("disease_name"):
686
+ searchable_text_parts.extend([
687
+ info.get("disease_name", ""),
688
+ info.get("description", ""),
689
+ info.get("crop", ""),
690
+ info.get("type", ""),
691
+ " ".join(info.get("symptoms", [])),
692
+ " ".join(info.get("common_names", []))
693
+ ])
694
+
695
+ searchable_text = " ".join(searchable_text_parts).lower()
696
 
697
  # Check for exact substring matches
698
  if query_lower in searchable_text:
699
  exact_matches.append(SearchResult(
700
  class_name=class_name,
701
  class_id=create_class_id(class_name),
702
+ disease_info=safe_create_disease_info(class_name, info if info.get("disease_name") else None)
703
  ))
704
  else:
705
  fuzzy_candidates.append((class_name, info, searchable_text))
 
714
 
715
  # Fuzzy search on candidates
716
  search_texts = [text for _, _, text in fuzzy_candidates]
717
+ if search_texts:
718
+ fuzzy_matches = process.extract(
719
+ query, search_texts, scorer=fuzz.token_sort_ratio, limit=limit
720
+ )
721
+
722
+ suggestions = []
723
+ for match_text, score, idx in fuzzy_matches:
724
+ if score > 60: # Minimum relevance threshold
725
+ class_name, info, _ = fuzzy_candidates[idx]
726
+ suggestions.append(SearchResult(
727
+ class_name=class_name,
728
+ class_id=create_class_id(class_name),
729
+ disease_info=safe_create_disease_info(class_name, info if info.get("disease_name") else None),
730
+ relevance_score=score
731
+ ))
732
+
733
+ return SearchResponse(
734
+ results=[],
735
+ suggestions=suggestions,
736
+ total_results=len(suggestions),
737
+ message="No exact matches found. Showing relevant suggestions." if suggestions else "No matches found."
738
+ )
739
 
740
  return SearchResponse(
741
  results=[],
742
+ suggestions=[],
743
+ total_results=0,
744
+ message="No matches found."
745
  )
746
 
747
  @app.get("/diseases/{class_id}", response_model=SearchResult)
 
755
  # Decode the class_id back to class_name
756
  class_name = decode_class_id(class_id)
757
 
758
+ # Validate that the class exists in our CLASS_NAMES
759
+ if class_name not in CLASS_NAMES:
 
760
  raise HTTPException(
761
  status_code=status.HTTP_404_NOT_FOUND,
762
+ detail=f"Class with ID '{class_id}' not found in supported classes."
763
  )
764
 
765
+ disease_data = disease_guide.get(class_name, None)
766
+
767
  return SearchResult(
768
  class_name=class_name,
769
  class_id=class_id,
770
+ disease_info=safe_create_disease_info(class_name, disease_data)
771
  )
772
 
773
  except UnicodeDecodeError:
 
778
 
779
  @app.get("/diseases/by-name/{class_name}", response_model=SearchResult)
780
  async def get_disease_by_class_name(
781
+ class_name: str = Path(..., description="Exact class name (string number), e.g. '0', '1', '2'")
782
  ):
783
  """
784
  Retrieve detailed information for a specific disease class by exact class name
785
  (Alternative endpoint for direct class name access)
786
  """
787
+ # Validate that the class exists in our CLASS_NAMES
788
+ if class_name not in CLASS_NAMES:
 
789
  raise HTTPException(
790
  status_code=status.HTTP_404_NOT_FOUND,
791
+ detail=f"Class '{class_name}' not found in supported classes. Supported classes: {', '.join(CLASS_NAMES[:10])}..."
792
  )
793
 
794
+ disease_data = disease_guide.get(class_name, None)
795
  return SearchResult(
796
  class_name=class_name,
797
  class_id=create_class_id(class_name),
798
+ disease_info=safe_create_disease_info(class_name, disease_data)
799
  )
800
 
801
  @app.get("/stats")
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ