RockyBai commited on
Commit
ef0ef1f
·
verified ·
1 Parent(s): f476ca4

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +278 -2
api.py CHANGED
@@ -72,13 +72,32 @@ def check_spatial_duplicate(lat, lon, issue_type, current_time):
72
  if lat == 0 or lon == 0:
73
  return False, "No Location"
74
 
 
 
75
  for report in REPORT_HISTORY:
76
  # Check Time Window
77
  if (current_time - report['time']) > timedelta(hours=DEDUP_TIME_WINDOW_HOURS):
78
  continue
79
 
80
- # Check Issue Type
81
- if report['issue'] != issue_type:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  continue
83
 
84
  # Check Distance
@@ -88,6 +107,263 @@ def check_spatial_duplicate(lat, lon, issue_type, current_time):
88
 
89
  return False, None
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  def check_velocity_spam(user_email, current_time):
92
  """Check if user is submitting too frequently."""
93
  if not user_email:
 
72
  if lat == 0 or lon == 0:
73
  return False, "No Location"
74
 
75
+ issue_lower = issue_type.lower()
76
+
77
  for report in REPORT_HISTORY:
78
  # Check Time Window
79
  if (current_time - report['time']) > timedelta(hours=DEDUP_TIME_WINDOW_HOURS):
80
  continue
81
 
82
+ # Check Issue Type (Loose Match)
83
+ # If "garbage" in new and "garbage" in old, it's a match.
84
+ report_issue_lower = report['issue'].lower()
85
+
86
+ # Simple keyword overlap check
87
+ keywords = ["garbage", "pothole", "accident", "water", "streetlight"]
88
+ match = False
89
+
90
+ # 1. Exact match (case insensitive)
91
+ if issue_lower == report_issue_lower:
92
+ match = True
93
+ # 2. Keyword match
94
+ else:
95
+ for kw in keywords:
96
+ if kw in issue_lower and kw in report_issue_lower:
97
+ match = True
98
+ break
99
+
100
+ if not match:
101
  continue
102
 
103
  # Check Distance
 
107
 
108
  return False, None
109
 
110
+
111
+ def check_velocity_spam(user_email, current_time):
112
+ """Check if user is submitting too frequently."""
113
+ if not user_email:
114
+ return False
115
+
116
+ if user_email not in USER_ACTIVITY:
117
+ USER_ACTIVITY[user_email] = deque(maxlen=10)
118
+
119
+ timestamps = USER_ACTIVITY[user_email]
120
+ timestamps.append(current_time)
121
+
122
+ # Filter timestamps within the window
123
+ recent_activity = [t for t in timestamps if (current_time - t).total_seconds() <= SPAM_VELOCITY_WINDOW_SECONDS]
124
+
125
+ if len(recent_activity) > SPAM_VELOCITY_LIMIT:
126
+ return True
127
+ return False
128
+
129
+
130
+ @app.get("/")
131
+ def read_root():
132
+ return {"status": "Active", "service": "Arise AI Backend"}
133
+
134
+ # --- SYNC HISTORY ENDPOINT ---
135
+ from pydantic import BaseModel
136
+ from typing import List
137
+
138
+ class HistoryItem(BaseModel):
139
+ lat: float
140
+ lon: float
141
+ issue: str
142
+ time: float # Timestamp
143
+ user: str
144
+ hash: Optional[str] = None
145
+
146
+ @app.post("/sync-history")
147
+ async def sync_history(items: List[HistoryItem]):
148
+ """
149
+ Syncs recent history from the frontend (Firebase) to the backend.
150
+ This allows the backend to perform deduplication and spam checks against
151
+ data that persists across backend restarts.
152
+ """
153
+ count = 0
154
+ for item in items:
155
+ # Avoid re-adding if already known (simple check by time+user)
156
+ # In a real overlap scenario we might need a better unique ID, but this is enough for simple seeding.
157
+ # We only add if timestamp is within the last 24h window roughly.
158
+
159
+ # Add to REPORT_HISTORY
160
+ # Convert timestamp to datetime
161
+ dt = datetime.fromtimestamp(item.time / 1000.0) # JS sends ms
162
+
163
+ # Check if already exists (approximate)
164
+ if any(r['user'] == item.user and abs((r['time'] - dt).total_seconds()) < 1.0 for r in REPORT_HISTORY):
165
+ continue
166
+
167
+ REPORT_HISTORY.append({
168
+ 'lat': item.lat,
169
+ 'lon': item.lon,
170
+ 'issue': item.issue,
171
+ 'time': dt,
172
+ 'user': item.user,
173
+ 'hash': item.hash or "" # Allow empty hash for legacy without re-analysis
174
+ })
175
+
176
+ # Add to USER_ACTIVITY for velocity checks
177
+ if item.user:
178
+ if item.user not in USER_ACTIVITY:
179
+ USER_ACTIVITY[item.user] = deque(maxlen=10)
180
+ USER_ACTIVITY[item.user].append(dt)
181
+
182
+ count += 1
183
+
184
+ logger.info(f"Synced {count} distinct history items from frontend.")
185
+ return {"status": "success", "synced": count}
186
+
187
+ @app.post("/analyze")
188
+ async def analyze_endpoint(
189
+ image: UploadFile = File(...),
190
+ description: str = Form(""),
191
+ latitude: str = Form("0"),
192
+ longitude: str = Form("0"),
193
+ timestamp: str = Form(""),
194
+ user_email: str = Form(None)
195
+ ):
196
+ try:
197
+ # Parse inputs
198
+ try:
199
+ lat = float(latitude)
200
+ lon = float(longitude)
201
+ except ValueError:
202
+ lat, lon = 0.0, 0.0
203
+
204
+ current_time = datetime.now()
205
+
206
+ # Load Image
207
+ contents = await image.read()
208
+ pil_image = Image.open(io.BytesIO(contents)).convert("RGB")
209
+
210
+ # Handle EXIF Rotation
211
+ try:
212
+ pil_image = ImageOps.exif_transpose(pil_image)
213
+ except Exception:
214
+ pass # Keep original if EXIF fails
215
+
216
+ img_np = np.array(pil_image)
217
+
218
+ # --- ANALYSIS PHASE ---
219
+
220
+ # 1. Spam Detection
221
+ # A. Blur Check
222
+ gray = cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
223
+ blur_score = cv2.Laplacian(gray, cv2.CV_64F).var()
224
+ is_blur_spam = bool(blur_score < 100.0)
225
+
226
+ # B. Velocity Check
227
+ is_velocity_spam = check_velocity_spam(user_email, current_time)
228
+
229
+ is_spam = is_blur_spam or is_velocity_spam
230
+ spam_reason = []
231
+ if is_blur_spam: spam_reason.append(f"Image too blurry (Score: {int(blur_score)})")
232
+ if is_velocity_spam: spam_reason.append("Submission rate exceeded limit")
233
+
234
+ spam_reason_str = ", ".join(spam_reason) if spam_reason else None
235
+
236
+ # Run Inference
237
+ logger.info("Running YOLO inference...")
238
+ results = model(img_np, conf=0.1)
239
+
240
+ detections = []
241
+ primary_issue = "Unknown"
242
+ max_conf = 0.0
243
+
244
+ result = results[0]
245
+
246
+ # Analyze Detections
247
+ if len(result.boxes) > 0:
248
+ for box in result.boxes:
249
+ cls_id = int(box.cls)
250
+ conf = float(box.conf)
251
+ label = model.names[cls_id]
252
+
253
+ detections.append({
254
+ "class": label,
255
+ "confidence": conf
256
+ })
257
+
258
+ if conf > max_conf:
259
+ max_conf = conf
260
+ primary_issue = label
261
+
262
+ # Fallback: Check Description if YOLO fails
263
+ if primary_issue == "Unknown" and description:
264
+ logger.info(f"YOLO found no objects, checking description: {description}")
265
+ desc_lower = description.lower()
266
+ keywords = {
267
+ "pothole": "Pothole", "pathole": "Pothole", "hole": "Pothole", "road": "Pothole",
268
+ "garbage": "Garbage", "trash": "Garbage", "waste": "Garbage",
269
+ "street light": "Streetlight", "streetlight": "Streetlight", "light": "Streetlight",
270
+ "accident": "Accident", "collision": "Accident",
271
+ "water": "Drainagen", "drainage": "Drainagen", "leak": "Drainagen"
272
+ }
273
+
274
+ for key, val in keywords.items():
275
+ if key in desc_lower:
276
+ primary_issue = val
277
+ max_conf = 0.5 # Moderate confidence for text match
278
+ break
279
+
280
+ # 2. Deduplication detection
281
+ # A. Hash Check (Hamming Distance)
282
+ current_hash = imagehash.phash(pil_image)
283
+ phash_str = str(current_hash)
284
+
285
+ # B. Spatial Check
286
+ is_spatial_dup, spatial_msg = check_spatial_duplicate(lat, lon, primary_issue, current_time)
287
+
288
+ # Check hash against history using Hamming distance < 5
289
+ is_hash_dup = False
290
+ for r in REPORT_HISTORY:
291
+ try:
292
+ # Convert stored hex string back to hash object
293
+ stored_hash = imagehash.hex_to_hash(r['hash'])
294
+ if current_hash - stored_hash < 5:
295
+ is_hash_dup = True
296
+ break
297
+ except Exception:
298
+ continue
299
+
300
+ is_duplicate = is_hash_dup or is_spatial_dup
301
+ dup_reason = "Duplicate image detected" if is_hash_dup else (spatial_msg if is_spatial_dup else None)
302
+
303
+ # Update History
304
+ REPORT_HISTORY.append({
305
+ 'lat': lat,
306
+ 'lon': lon,
307
+ 'issue': primary_issue,
308
+ 'time': current_time,
309
+ 'user': user_email,
310
+ 'hash': phash_str
311
+ })
312
+
313
+ # Process Image for Overlay
314
+ annotated_frame = result.plot(line_width=2, font_size=1.0)
315
+ is_success, buffer = cv2.imencode(".jpg", cv2.cvtColor(annotated_frame, cv2.COLOR_RGB2BGR))
316
+ processed_image_base64 = None
317
+ if is_success:
318
+ import base64
319
+ processed_image_base64 = base64.b64encode(buffer).decode("utf-8")
320
+
321
+ # Map to Civicsense categories (Bangalore Specific)
322
+ category_map = {
323
+ "pothole": "BBMP - Road Infrastructure",
324
+ "garbage": "BBMP - Solid Waste Management",
325
+ "streetlight": "BESCOM - Street Lights",
326
+ "accident": "Traffic Police / Emergency",
327
+ "drainagen": "BWSSB - Water & Sewerage",
328
+ "water": "BWSSB - Water Supply"
329
+ }
330
+
331
+ department = category_map.get(primary_issue.lower(), "General")
332
+
333
+ # Generate AI Summary (Text Only, No bold markers)
334
+ summary_lines = []
335
+
336
+ # Line 1: Identification
337
+ if primary_issue != "Unknown":
338
+ summary_lines.append(f"Identification: AI detected {primary_issue} with {int(max_conf*100)}% confidence.")
339
+ else:
340
+ summary_lines.append("Identification: No specific civic issue could be confidently identified.")
341
+
342
+ # Line 2: Quality Analysis
343
+ if is_blur_spam:
344
+ summary_lines.append(f"Image Quality: Poor/Blurry (Score: {int(blur_score)}/100). Please retake.")
345
+ else:
346
+ summary_lines.append(f"Image Quality: Good clarity (Score: {int(blur_score)}/100).")
347
+
348
+ # Line 3: Assessment
349
+ summary_lines.append(f"Assessment: Rated as {severity} severity, routed to {department}.")
350
+
351
+ # Line 4: Status/Warnings
352
+ status_parts = []
353
+ if is_duplicate:
354
+ status_parts.append(f"Duplicate: {dup_reason}.")
355
+ if is_spam:
356
+ status_parts.append(f"Spam Flag: {spam_reason_str}.")
357
+
358
+ if not status_parts:
359
+ status_parts.append("Status: Verified as a unique, valid report.")
360
+
361
+ summary_lines.append(" ".join(status_parts))
362
+
363
+ ai_summary = "\n".join(summary_lines)
364
+
365
+ response_data = {
366
+
367
  def check_velocity_spam(user_email, current_time):
368
  """Check if user is submitting too frequently."""
369
  if not user_email: