Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/hf_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ async def _detect_clip_generic(image: Union[Image.Image, bytes], labels: List[st

# --- Specific Detectors ---

async def detect_pothole_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
labels = ["pothole", "damaged road", "road crack", "smooth road", "clean street"]
targets = ["pothole", "damaged road", "road crack"]
return await _detect_clip_generic(image, labels, targets, client)

async def detect_illegal_parking_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
labels = ["illegal parking", "car blocking driveway", "double parked", "car on sidewalk", "legal parking", "empty street"]
targets = ["illegal parking", "car blocking driveway", "double parked", "car on sidewalk"]
Expand Down
19 changes: 14 additions & 5 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
find_mla_by_constituency
)
from backend.init_db import migrate_db
from backend.pothole_detection import detect_potholes, validate_image_for_processing
from backend.pothole_detection import detect_potholes, validate_image_for_processing, is_model_available
from backend.grievance_service import GrievanceService
from backend.unified_detection_service import (
detect_vandalism as detect_vandalism_unified,
Expand Down Expand Up @@ -82,7 +82,8 @@
detect_audio_event,
transcribe_audio,
detect_waste_clip,
detect_civic_eye_clip
detect_civic_eye_clip,
detect_pothole_clip
)

# Configure structured logging
Expand Down Expand Up @@ -1109,7 +1110,7 @@ def get_recent_issues(db: Session = Depends(get_db)):
return data

@app.post("/api/detect-pothole", response_model=DetectionResponse)
async def detect_pothole_endpoint(image: UploadFile = File(...)):
async def detect_pothole_endpoint(request: Request, image: UploadFile = File(...)):
# Validate uploaded file
await validate_uploaded_file(image)

Expand All @@ -1124,9 +1125,17 @@ async def detect_pothole_endpoint(image: UploadFile = File(...)):
logger.error(f"Invalid image file for pothole detection: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid image file")

# Run detection (blocking, so run in threadpool)
# Run detection
try:
detections = await run_in_threadpool(detect_potholes, pil_image)
if is_model_available():
# Use local model if available (blocking)
detections = await run_in_threadpool(detect_potholes, pil_image)
else:
# Fallback to Hugging Face API
logger.info("Local pothole model unavailable, falling back to HF API")
client = request.app.state.http_client
detections = await detect_pothole_clip(pil_image, client=client)
Comment on lines +1130 to +1137
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detect_pothole_endpoint decides between local vs HF using is_model_available(), but that only checks whether ultralyticsplus can be imported. If the import succeeds but load_model() returns None (e.g., model download fails or other runtime error), this endpoint will still choose the local path and return empty detections instead of falling back to HF. Consider basing the decision on whether the model actually loaded (e.g., call get_model() once at startup / cache a readiness flag) or catch a local-model failure and then invoke detect_pothole_clip as fallback.

Copilot uses AI. Check for mistakes.

return DetectionResponse(detections=detections)
except Exception as e:
logger.error(f"Pothole detection error: {e}", exc_info=True)
Expand Down
18 changes: 15 additions & 3 deletions backend/pothole_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,15 @@
_model_loading_error: Optional[Exception] = None
_model_initialized: bool = False

_model = None
_model_lock = threading.Lock()
def is_model_available():
"""
Checks if the model dependencies are available.
"""
try:
import ultralyticsplus
return True
except ImportError:
return False

def load_model():
"""
Expand Down Expand Up @@ -44,9 +51,12 @@ def load_model():

logger.info("Model loaded successfully.")
return model
except ImportError:
logger.warning("ultralyticsplus not installed. Pothole detection disabled.")
return None
except Exception as e:
logger.error(f"Failed to load model: {e}")
raise ModelLoadException("keremberke/yolov8n-pothole-segmentation", details={"error": str(e)}) from e
return None
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

load_model() swallows all non-ImportError exceptions and returns None. This causes get_model() to treat initialization as successful and cache a None model, making subsequent detections silently return [] with no error signal for callers to trigger a fallback. Consider re-raising (or wrapping in ModelLoadException) for unexpected load failures so callers can distinguish “model unavailable” from “no potholes found.”

Suggested change
return None
raise

Copilot uses AI. Check for mistakes.


def get_model():
Expand Down Expand Up @@ -147,6 +157,8 @@ def detect_potholes(image_source):
"""
try:
model = get_model()
if model is None:
return []
Copy link

Copilot AI Feb 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning an empty list when model is None makes a model-load/config problem indistinguishable from a real “no detections” result. If the API is expected to fall back to HF when the local model can’t run, consider raising a dedicated exception here (or in get_model()) so callers can reliably detect and handle the failure mode.

Suggested change
return []
logger.error("Pothole detection model is not available (get_model() returned None).")
raise DetectionException(
"Pothole detection model is not available",
"pothole",
details={"error": "model_unavailable"}
)

Copilot uses AI. Check for mistakes.

Comment on lines +160 to 162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return value masks “model unavailable” vs “no detections.”

An empty list is also a valid “no potholes” result, so callers can’t distinguish a load failure. Consider raising ModelLoadException (or another sentinel) so the caller can fall back or surface a 503.

🐛 Suggested change to surface model-unavailable explicitly
-        if model is None:
-            return []
+        if model is None:
+            raise ModelLoadException(
+                "keremberke/yolov8n-pothole-segmentation",
+                details={"error": "model unavailable"}
+            )
@@
-    except Exception as e:
+    except ModelLoadException:
+        raise
+    except Exception as e:
🤖 Prompt for AI Agents
In `@backend/pothole_detection.py` around lines 160 - 162, The current early
return "if model is None: return []" in backend/pothole_detection.py conflates
"model unavailable" with "no detections"; replace that empty-list return with
raising a specific sentinel exception (e.g., ModelLoadException) so callers can
distinguish failures from valid empty results, and add/declare the
ModelLoadException class (or reuse an existing error type) and update the
function docstring/signature where this check occurs to document the raised
exception so callers can catch it and respond (e.g., return 503).

# perform inference
# stream=False ensures we get all results in memory
Expand Down
6 changes: 0 additions & 6 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,10 @@ google-generativeai
python-multipart
psycopg2-binary
async-lru
ultralyticsplus==0.0.28
ultralytics
opencv-python-headless
huggingface-hub
httpx
python-magic
pywebpush
# Local ML dependencies (Issue #76)
torch
transformers
Pillow
firebase-functions
firebase-admin
Expand Down
35 changes: 10 additions & 25 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions frontend/public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/*
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
1 change: 1 addition & 0 deletions frontend/public/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* /index.html 200
Loading