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
14 changes: 13 additions & 1 deletion backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class Config:
secret_key: str
algorithm: str
access_token_expire_minutes: int

hf_token: Optional[str] = None

@classmethod
def from_env(cls) -> "Config":
Expand All @@ -65,6 +67,8 @@ def from_env(cls) -> "Config":
telegram_bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
if not telegram_bot_token:
errors.append("TELEGRAM_BOT_TOKEN is required")

hf_token = os.getenv("HF_TOKEN")

# Database with default
database_url = os.getenv(
Expand Down Expand Up @@ -121,6 +125,7 @@ def from_env(cls) -> "Config":
return cls(
gemini_api_key=gemini_api_key,
telegram_bot_token=telegram_bot_token,
hf_token=hf_token,
database_url=database_url,
environment=environment,
debug=debug,
Expand Down Expand Up @@ -160,6 +165,8 @@ def validate_api_keys(self) -> dict[str, bool]:
"gemini_api_key": len(self.gemini_api_key) > 20,
"telegram_bot_token": ":" in self.telegram_bot_token and len(self.telegram_bot_token) > 40,
}
if self.hf_token:
validations["hf_token"] = len(self.hf_token) > 10
return validations

def __repr__(self) -> str:
Expand All @@ -170,7 +177,8 @@ def __repr__(self) -> str:
f" database={self.get_database_type()},\n"
f" debug={self.debug},\n"
f" gemini_api_key={'*' * 10 if self.gemini_api_key else 'NOT SET'},\n"
f" telegram_bot_token={'*' * 10 if self.telegram_bot_token else 'NOT SET'}\n"
f" telegram_bot_token={'*' * 10 if self.telegram_bot_token else 'NOT SET'},\n"
f" hf_token={'*' * 10 if self.hf_token else 'NOT SET'}\n"
f")"
)

Expand Down Expand Up @@ -273,6 +281,10 @@ def get_telegram_bot_token() -> str:
"""Get Telegram bot token from config."""
return get_config().telegram_bot_token

def get_hf_token() -> Optional[str]:
"""Get Hugging Face token from config."""
return get_config().hf_token
Comment on lines +284 to +286
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

Avoid forcing full app config load when only HF token is needed.

get_hf_token() calls get_config(), which can exit the process if unrelated required keys are missing. That makes HF-only code paths brittle.

Suggested fix
 def get_hf_token() -> Optional[str]:
     """Get Hugging Face token from config."""
-    return get_config().hf_token
+    global _config
+    if _config is not None:
+        return _config.hf_token
+    # Allow HF token lookup without forcing full Config bootstrap
+    return os.getenv("HF_TOKEN")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/config.py` around lines 284 - 286, get_hf_token() currently calls
get_config() which may abort the process if unrelated required keys are missing;
change get_hf_token to avoid invoking get_config() and instead fetch the Hugging
Face token directly (e.g., read from environment variables or a lightweight
config accessor) so HF-only code paths don't trigger full-app
validation/exit—update the function get_hf_token() to return the HF token from
os.environ (or a safe partial config read) and ensure it never calls
get_config() or any routine that can sys.exit() on missing keys.



def get_database_url() -> str:
"""Get database URL from config."""
Expand Down
27 changes: 26 additions & 1 deletion backend/hf_api_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
from typing import Union, List, Dict, Any
from PIL import Image
import logging
from backend.config import get_hf_token

logger = logging.getLogger(__name__)

# HF_TOKEN should be set in environment variables
token = os.environ.get("HF_TOKEN")
token = get_hf_token()
headers = {"Authorization": f"Bearer {token}"} if token else {}
Comment on lines +13 to 14
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

Compute auth headers at request time, not module import time.

Capturing token/headers once at import can become stale and couples request auth to import order. Build headers lazily per call.

Suggested fix
-token = get_hf_token()
-headers = {"Authorization": f"Bearer {token}"} if token else {}
+def _auth_headers() -> Dict[str, str]:
+    token = get_hf_token()
+    return {"Authorization": f"Bearer {token}"} if token else {}
@@
-        response = await client.post(url, headers=headers, json=payload, timeout=20.0)
+        response = await client.post(url, headers=_auth_headers(), json=payload, timeout=20.0)

Apply the same _auth_headers() helper in the binary upload paths currently using headers_bin.

🤖 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 13 - 14, The module currently
captures get_hf_token() into module-level variables `token` and `headers`, which
can become stale; change auth header construction to be lazy by adding/using a
helper `_auth_headers()` that calls `get_hf_token()` and returns
{"Authorization": f"Bearer {token}"} or {} per request, then replace all uses of
the module-level `headers` and `headers_bin` with calls to `_auth_headers()`
(including in binary upload paths) so each request computes fresh auth headers.


# Zero-Shot Image Classification Model
Expand Down Expand Up @@ -456,3 +457,27 @@ 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_vandalism_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
"""
Detects vandalism/graffiti.
"""
labels = ["graffiti", "vandalism", "spray paint", "street art", "clean wall", "public property", "normal street"]
targets = ["graffiti", "vandalism", "spray paint"]
return await _detect_clip_generic(image, labels, targets, client)

async def detect_infrastructure_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
"""
Detects general infrastructure damage.
"""
labels = ["broken streetlight", "damaged traffic sign", "fallen tree", "damaged fence", "pothole", "clean street", "normal infrastructure"]
targets = ["broken streetlight", "damaged traffic sign", "fallen tree", "damaged fence"]
return await _detect_clip_generic(image, labels, targets, client)

async def detect_flooding_clip(image: Union[Image.Image, bytes], client: httpx.AsyncClient = None):
"""
Detects flooding/waterlogging (outdoor).
"""
labels = ["flooded street", "waterlogging", "blocked drain", "heavy rain", "dry street", "normal road"]
targets = ["flooded street", "waterlogging", "blocked drain", "heavy rain"]
return await _detect_clip_generic(image, labels, targets, client)
Comment on lines +461 to +483
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

These three detection functions (detect_vandalism_clip, detect_infrastructure_clip, detect_flooding_clip) are added to support the unified_detection_service but appear to duplicate existing functionality. The detect_vandalism_clip function seems to overlap with the existing detect_graffiti_art_clip function (line 437-443). Similarly, infrastructure and flooding detection functions may duplicate existing endpoints. Consider whether these are necessary or if the existing functions could be reused instead.

Copilot uses AI. Check for mistakes.
6 changes: 3 additions & 3 deletions backend/unified_detection_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ async def detect_vandalism(self, image: Image.Image) -> List[Dict]:
return await detect_vandalism_local(image)

elif backend == "huggingface":
from hf_service import detect_vandalism_clip
from backend.hf_api_service import detect_vandalism_clip
return await detect_vandalism_clip(image)

else:
Expand Down Expand Up @@ -155,7 +155,7 @@ async def detect_infrastructure(self, image: Image.Image) -> List[Dict]:
return await detect_infrastructure_local(image)

elif backend == "huggingface":
from hf_service import detect_infrastructure_clip
from backend.hf_api_service import detect_infrastructure_clip
return await detect_infrastructure_clip(image)

else:
Expand Down Expand Up @@ -183,7 +183,7 @@ async def detect_flooding(self, image: Image.Image) -> List[Dict]:
return await detect_flooding_local(image)

elif backend == "huggingface":
from hf_service import detect_flooding_clip
from backend.hf_api_service import detect_flooding_clip
return await detect_flooding_clip(image)

else:
Expand Down
3 changes: 2 additions & 1 deletion frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'

export default defineConfig([
globalIgnores(['dist']),
globalIgnores(['dist', 'src/setupTests.js', '**/*.test.js', '**/__tests__/**']),
{
files: ['**/*.{js,jsx}'],
ignores: ['**/__mocks__/**'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
Expand Down
Loading