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
46 changes: 46 additions & 0 deletions backend/hf_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,49 @@ async def detect_abandoned_vehicle_clip(image: Union[Image.Image, bytes], client
labels = ["abandoned car", "rusted vehicle", "car with flat tires", "wrecked car", "normal parked car"]
targets = ["abandoned car", "rusted vehicle", "car with flat tires", "wrecked car"]
return await _detect_clip_generic(image, labels, targets, client)

async def detect_air_quality_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
"""
Detects air quality/smog levels using CLIP.
"""
labels = ["clean air", "mild smog", "dense smog", "hazardous air pollution", "fog", "clear sky", "blue sky", "polluted city"]
targets = ["mild smog", "dense smog", "hazardous air pollution", "fog", "polluted city"]
return await _detect_clip_generic(image, labels, targets, client)
Comment on lines +460 to +466
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"fog" classified as pollution may produce false positives.

The targets list includes "fog", which is a natural weather phenomenon and not necessarily an indicator of air pollution. This could cause clean-air scenes with natural fog to be flagged as polluted. Consider removing "fog" from targets or adding a separate neutral category.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/hf_api_service.py` around lines 460 - 466, The targets for pollution
detection in detect_air_quality_clip include "fog", which can cause false
positives for natural fog; update detect_air_quality_clip so targets no longer
treat "fog" as pollution (remove "fog" from the targets list) or alternatively
add a neutral/ambiguous category and adjust the call to _detect_clip_generic
accordingly (e.g., keep "fog" in labels but not in targets and/or add a separate
neutral_labels list and handle it in _detect_clip_generic or post-process its
results).


async def detect_cleanliness_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
"""
Verifies if a wall/area is clean (for graffiti removal verification).
"""
labels = ["clean wall", "graffiti", "vandalism", "freshly painted wall", "dirty wall"]
# We want to know if it is CLEAN
targets = ["clean wall", "freshly painted wall"]
return await _detect_clip_generic(image, labels, targets, client)
Comment on lines +468 to +475
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Cleanliness detector only returns positive matches — dirty walls produce empty results, breaking frontend UX.

detect_cleanliness_clip targets only ["clean wall", "freshly painted wall"]. When a wall has graffiti, the top CLIP labels will be "graffiti", "vandalism", or "dirty wall" — none of which are in target_labels. _detect_clip_generic (line 81) filters to only target labels with score > 0.4, so the function returns [] for dirty walls.

In GraffitiVerification.jsx, an empty detection list results in { label: "Unable to determine", confidence: 0 } — which is neither a pass nor a clear fail. The user gets an ambiguous result instead of a definitive "wall has graffiti" message.

Consider returning the top result regardless of target membership, or include negative labels in targets so the frontend can distinguish clean from dirty.

Proposed fix: return all results and let the frontend decide
 async def detect_cleanliness_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
     """
     Verifies if a wall/area is clean (for graffiti removal verification).
     """
     labels = ["clean wall", "graffiti", "vandalism", "freshly painted wall", "dirty wall"]
-    # We want to know if it is CLEAN
-    targets = ["clean wall", "freshly painted wall"]
-    return await _detect_clip_generic(image, labels, targets, client)
+    try:
+        img_bytes = _prepare_image_bytes(image)
+        results = await query_hf_api(img_bytes, labels, client=client)
+        if not isinstance(results, list) or len(results) == 0:
+            return []
+        top = results[0]
+        return [{
+            "label": top.get('label', 'unknown'),
+            "confidence": top.get('score', 0),
+            "box": []
+        }]
+    except Exception as e:
+        logger.error(f"Cleanliness Detection Error: {e}")
+        return []
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/hf_api_service.py` around lines 468 - 475, The detector currently
only passes positive targets to _detect_clip_generic (targets = ["clean
wall","freshly painted wall"]), so dirty walls produce an empty list; change
detect_cleanliness_clip to return full CLIP results so the frontend can decide:
call _detect_clip_generic with either targets set to the full labels list
(labels) or with a flag/None that disables target filtering (so all top labels
like "graffiti", "vandalism", "dirty wall" are returned), leaving
GraffitiVerification.jsx able to interpret pass/fail; update
detect_cleanliness_clip (and if needed _detect_clip_generic) accordingly.


async def detect_noise_pollution_event(audio_bytes: bytes, client: httpx.AsyncClient = None):
"""
Wraps detect_audio_event to flag noise pollution.
"""
events = await detect_audio_event(audio_bytes, client)

noise_pollution_labels = [
"traffic", "horn", "siren", "jackhammer", "construction", "drill",
"engine", "explosion", "gunshot", "scream", "shout", "bark", "chainsaw",
"aircraft", "helicopter", "train"
]

detected_noise = []
for event in events:
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 17, 2026

Choose a reason for hiding this comment

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

P2: Missing type validation on events before iteration. If detect_audio_event returns a non-list value (e.g., a dict from an unexpected API response), iterating and calling .get() on the elements will raise an AttributeError. The codebase convention (see analyze_image_full_clip) is to check isinstance(results, list) before processing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/hf_api_service.py, line 490:

<comment>Missing type validation on `events` before iteration. If `detect_audio_event` returns a non-list value (e.g., a dict from an unexpected API response), iterating and calling `.get()` on the elements will raise an `AttributeError`. The codebase convention (see `analyze_image_full_clip`) is to check `isinstance(results, list)` before processing.</comment>

<file context>
@@ -456,3 +456,49 @@ async def detect_abandoned_vehicle_clip(image: Union[Image.Image, bytes], client
+    ]
+
+    detected_noise = []
+    for event in events:
+        # MIT/AST labels might be specific, e.g. "Car alarm"
+        label = event.get('label', '').lower()
</file context>
Fix with Cubic

# MIT/AST labels might be specific, e.g. "Car alarm"
label = event.get('label', '').lower()
score = event.get('score', 0)

is_noise = any(nl in label for nl in noise_pollution_labels)

if is_noise and score > 0.3:
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The confidence threshold of 0.3 for noise pollution detection is relatively low and may result in false positives. Consider documenting why this threshold was chosen or making it configurable. Other detection functions in the codebase typically don't filter by score, leaving that decision to the caller.

Copilot uses AI. Check for mistakes.
detected_noise.append({
"type": event.get('label'),
"confidence": score,
"is_pollution": True
})

return detected_noise
Comment on lines +477 to +504
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The detect_noise_pollution_event function only returns events that match the noise pollution criteria and filters out all other sounds. This means when non-pollution sounds are detected (like speech or music), the frontend receives an empty array and shows "Listening..." even though sounds were actually detected. Consider returning all detected sounds with the is_pollution flag set to false for non-pollution events, so users get feedback that the system is working and detecting sounds even when there's no pollution.

Copilot uses AI. Check for mistakes.
6 changes: 4 additions & 2 deletions backend/requirements-render.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ Pillow
firebase-functions
firebase-admin
a2wsgi
python-jose[cryptography]
passlib[bcrypt]
python-jose
cryptography
Comment on lines +16 to +17
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 17, 2026

Choose a reason for hiding this comment

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

P2: Prefer python-jose[cryptography] over splitting into separate packages. The python-jose docs state: "The backend must be selected as an extra when installing python-jose." While it works because cryptography is installed separately and detected at runtime, using the extras syntax is the documented approach, preserves version constraints, and avoids pulling in unnecessary native-python backend dependencies (rsa, ecdsa, pyasn1). The same applies to passlib[bcrypt] — passlib documents pip install passlib[bcrypt] as the recommended installation method.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/requirements-render.txt, line 16:

<comment>Prefer `python-jose[cryptography]` over splitting into separate packages. The python-jose docs state: "The backend must be selected as an extra when installing python-jose." While it works because `cryptography` is installed separately and detected at runtime, using the extras syntax is the documented approach, preserves version constraints, and avoids pulling in unnecessary native-python backend dependencies (`rsa`, `ecdsa`, `pyasn1`). The same applies to `passlib[bcrypt]` — passlib documents `pip install passlib[bcrypt]` as the recommended installation method.</comment>

<file context>
@@ -13,5 +13,7 @@ Pillow
 a2wsgi
-python-jose[cryptography]
-passlib[bcrypt]
+python-jose
+cryptography
+passlib
</file context>
Suggested change
python-jose
cryptography
python-jose[cryptography]
Fix with Cubic

passlib
bcrypt
51 changes: 50 additions & 1 deletion backend/routers/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@
detect_civic_eye_clip,
detect_graffiti_art_clip,
detect_traffic_sign_clip,
detect_abandoned_vehicle_clip
detect_abandoned_vehicle_clip,
detect_air_quality_clip,
detect_cleanliness_clip,
detect_noise_pollution_event
)
from backend.dependencies import get_http_client
import backend.dependencies
Expand Down Expand Up @@ -436,3 +439,49 @@ async def detect_abandoned_vehicle_endpoint(request: Request, image: UploadFile
except Exception as e:
logger.error(f"Abandoned vehicle detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@router.post("/api/detect-air-quality")
async def detect_air_quality_endpoint(request: Request, image: UploadFile = File(...)):
# Optimized Image Processing: Validation + Optimization
_, image_bytes = await process_uploaded_image(image)

try:
client = get_http_client(request)
detections = await detect_air_quality_clip(image_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Air quality detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@router.post("/api/detect-cleanliness")
async def detect_cleanliness_endpoint(request: Request, image: UploadFile = File(...)):
# Optimized Image Processing: Validation + Optimization
_, image_bytes = await process_uploaded_image(image)

try:
client = get_http_client(request)
detections = await detect_cleanliness_clip(image_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Cleanliness detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@router.post("/api/detect-noise-pollution")
async def detect_noise_pollution_endpoint(request: Request, file: UploadFile = File(...)):
# Audio validation
if hasattr(file, 'size') and file.size and file.size > 10 * 1024 * 1024:
raise HTTPException(status_code=413, detail="Audio file too large")

try:
audio_bytes = await file.read()
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 17, 2026

Choose a reason for hiding this comment

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

P1: Missing post-read file size validation. Unlike the existing /api/detect-audio endpoint, this endpoint only checks file.size (which may be unavailable for streamed/blob uploads) and does not validate len(audio_bytes) after reading. An oversized file can bypass the pre-read guard and be fully loaded into memory.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/routers/detection.py, line 476:

<comment>Missing post-read file size validation. Unlike the existing `/api/detect-audio` endpoint, this endpoint only checks `file.size` (which may be unavailable for streamed/blob uploads) and does not validate `len(audio_bytes)` after reading. An oversized file can bypass the pre-read guard and be fully loaded into memory.</comment>

<file context>
@@ -436,3 +439,49 @@ async def detect_abandoned_vehicle_endpoint(request: Request, image: UploadFile
+         raise HTTPException(status_code=413, detail="Audio file too large")
+
+    try:
+        audio_bytes = await file.read()
+    except Exception as e:
+        logger.error(f"Invalid audio file: {e}", exc_info=True)
</file context>
Suggested change
audio_bytes = await file.read()
audio_bytes = await file.read()
if len(audio_bytes) > 10 * 1024 * 1024:
raise HTTPException(status_code=413, detail="Audio file too large")
Fix with Cubic

except Exception as e:
logger.error(f"Invalid audio file: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid audio file")
Comment on lines +471 to +479
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The audio file validation logic is duplicated from the existing /api/detect-audio endpoint (lines 256-285). Consider extracting this validation logic into a shared helper function to reduce code duplication and ensure consistent validation across audio endpoints.

Copilot uses AI. Check for mistakes.

try:
client = get_http_client(request)
detections = await detect_noise_pollution_event(audio_bytes, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Noise pollution detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")
Comment on lines +443 to +487
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The new detection endpoints (detect-air-quality, detect-cleanliness, detect-noise-pollution) lack test coverage. Other similar detection endpoints in the codebase have corresponding tests in backend/tests/test_new_detectors.py. Tests should be added to verify the endpoints work correctly and handle errors appropriately.

Copilot uses AI. Check for mistakes.
Comment on lines +469 to +487
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing post-read file size validation for audio bytes.

The existing /api/detect-audio endpoint (line 273) validates len(audio_bytes) after reading, because file.size can be None for streamed/blob uploads. This new endpoint only checks file.size before the read (line 472), which is unreliable. An oversized file could bypass the check.

Proposed fix: add post-read size check
     try:
         audio_bytes = await file.read()
+        if len(audio_bytes) > 10 * 1024 * 1024:
+            raise HTTPException(status_code=413, detail="Audio file too large")
     except Exception as e:
         logger.error(f"Invalid audio file: {e}", exc_info=True)
         raise HTTPException(status_code=400, detail="Invalid audio file")
🧰 Tools
🪛 Ruff (0.15.0)

[warning] 470-470: Do not perform function call File in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable

(B008)


[warning] 479-479: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 484-484: Consider moving this statement to an else block

(TRY300)


[warning] 487-487: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/routers/detection.py` around lines 469 - 487, The endpoint
detect_noise_pollution_endpoint validates file.size before reading but misses
the necessary post-read check, so add a check immediately after reading
audio_bytes (i.e., after await file.read()) to raise HTTPException 413 if
len(audio_bytes) > 10 * 1024 * 1024; keep the existing error handling around
reading and only call detect_noise_pollution_event(client=client, audio_bytes)
after the size check to prevent oversized uploads from being processed.

Loading