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
28 changes: 28 additions & 0 deletions backend/hf_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,34 @@ async def detect_vandalism_clip(image: Image.Image, client: httpx.AsyncClient =
print(f"HF Detection Error: {e}")
return []

async def detect_tree_clip(image: Image.Image, client: httpx.AsyncClient = None):
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The new detect_tree_clip function is missing a docstring, unlike the other detection functions in this file (e.g., detect_vandalism_clip has a docstring). Add a docstring to describe the function's purpose, parameters, and return value for consistency and better maintainability.

Suggested change
async def detect_tree_clip(image: Image.Image, client: httpx.AsyncClient = None):
async def detect_tree_clip(image: Image.Image, client: httpx.AsyncClient = None):
"""
Detects tree-related hazards using Zero-Shot Image Classification with CLIP (Async).
Args:
image (PIL.Image.Image): The input image to analyze.
client (httpx.AsyncClient, optional): An optional shared HTTP client instance
to use for making the Hugging Face API request. If not provided, a new
client will be created internally.
Returns:
list[dict]: A list of detected tree-related issues, where each dict contains:
- "label": The predicted label string.
- "confidence": The confidence score for the label (float).
- "box": An empty list placeholder for bounding box data.
"""

Copilot uses AI. Check for mistakes.
try:
labels = ["fallen tree", "dangling branch", "overgrown vegetation", "tree blocking road", "healthy tree", "normal park"]

img_byte_arr = io.BytesIO()
image.save(img_byte_arr, format=image.format if image.format else 'JPEG')
img_bytes = img_byte_arr.getvalue()

results = await query_hf_api(img_bytes, labels, client=client)

if not isinstance(results, list):
return []

tree_labels = ["fallen tree", "dangling branch", "overgrown vegetation", "tree blocking road"]
detected = []

for res in results:
if isinstance(res, dict) and res.get('label') in tree_labels and res.get('score', 0) > 0.4:
detected.append({
"label": res['label'],
"confidence": res['score'],
"box": []
})
return detected
except Exception as e:
print(f"HF Detection Error: {e}")
return []

async def detect_infrastructure_clip(image: Image.Image, client: httpx.AsyncClient = None):
try:
labels = ["broken streetlight", "damaged traffic sign", "fallen tree", "damaged fence", "pothole", "clean street", "normal infrastructure"]
Expand Down
19 changes: 18 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
detect_street_light_clip,
detect_fire_clip,
detect_stray_animal_clip,
detect_blocked_road_clip
detect_blocked_road_clip,
detect_tree_clip
)
from PIL import Image
from init_db import migrate_db
Expand Down Expand Up @@ -483,6 +484,22 @@ async def detect_blocked_road_endpoint(request: Request, image: UploadFile = Fil
logger.error(f"Blocked road detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")

@app.post("/api/detect-tree-hazard")
async def detect_tree_hazard_endpoint(request: Request, image: UploadFile = File(...)):
try:
pil_image = await run_in_threadpool(Image.open, image.file)
except Exception as e:
logger.error(f"Invalid image file: {e}", exc_info=True)
raise HTTPException(status_code=400, detail="Invalid image file")

try:
client = request.app.state.http_client
detections = await detect_tree_clip(pil_image, client=client)
return {"detections": detections}
except Exception as e:
logger.error(f"Tree hazard detection error: {e}", exc_info=True)
raise HTTPException(status_code=500, detail="Internal server error")


@app.get("/api/mh/rep-contacts")
async def get_maharashtra_rep_contacts(pincode: str = Query(..., min_length=6, max_length=6)):
Expand Down
12 changes: 12 additions & 0 deletions frontend/package-lock.json

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

4 changes: 3 additions & 1 deletion frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const StreetLightDetector = React.lazy(() => import('./StreetLightDetector'));
const FireDetector = React.lazy(() => import('./FireDetector'));
const StrayAnimalDetector = React.lazy(() => import('./StrayAnimalDetector'));
const BlockedRoadDetector = React.lazy(() => import('./BlockedRoadDetector'));
const TreeDetector = React.lazy(() => import('./TreeDetector'));

// Get API URL from environment variable, fallback to relative URL for local dev
const API_URL = import.meta.env.VITE_API_URL || '';
Expand All @@ -38,7 +39,7 @@ function AppContent() {

// Safe navigation helper
const navigateToView = (view) => {
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked'];
const validViews = ['home', 'map', 'report', 'action', 'mh-rep', 'pothole', 'garbage', 'vandalism', 'flood', 'infrastructure', 'parking', 'streetlight', 'fire', 'animal', 'blocked', 'tree'];
if (validViews.includes(view)) {
navigate(view === 'home' ? '/' : `/${view}`);
}
Expand Down Expand Up @@ -217,6 +218,7 @@ function AppContent() {
<Route path="/fire" element={<FireDetector onBack={() => navigate('/')} />} />
<Route path="/animal" element={<StrayAnimalDetector onBack={() => navigate('/')} />} />
<Route path="/blocked" element={<BlockedRoadDetector onBack={() => navigate('/')} />} />
<Route path="/tree" element={<TreeDetector onBack={() => navigate('/')} />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
Expand Down
138 changes: 138 additions & 0 deletions frontend/src/TreeDetector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { useState, useRef, useCallback } from 'react';
import Webcam from 'react-webcam';

const TreeDetector = ({ onBack }) => {
const webcamRef = useRef(null);
const [imgSrc, setImgSrc] = useState(null);
const [detections, setDetections] = useState([]);
const [loading, setLoading] = useState(false);
const [cameraError, setCameraError] = useState(null);

const capture = useCallback(() => {
const imageSrc = webcamRef.current.getScreenshot();
setImgSrc(imageSrc);
}, [webcamRef]);

const retake = () => {
setImgSrc(null);
setDetections([]);
};

const detectTreeHazard = async () => {
if (!imgSrc) return;
setLoading(true);
setDetections([]);

try {
// Convert base64 to blob
const res = await fetch(imgSrc);
const blob = await res.blob();
const file = new File([blob], "image.jpg", { type: "image/jpeg" });

const formData = new FormData();
formData.append('image', file);

// Call Backend API
const response = await fetch('/api/detect-tree-hazard', {
method: 'POST',
body: formData,
});

if (response.ok) {
const data = await response.json();
setDetections(data.detections);
if (data.detections.length === 0) {
alert("No tree hazard detected.");
}
} else {
console.error("Detection failed");
alert("Detection failed. Please try again.");
}
} catch (error) {
console.error("Error:", error);
alert("An error occurred during detection.");
} finally {
setLoading(false);
}
};

return (
<div className="p-4 max-w-md mx-auto h-full flex flex-col">
<button onClick={onBack} className="self-start text-blue-600 mb-2">
&larr; Back
</button>
<h2 className="text-2xl font-bold mb-4 text-green-800">Tree Hazard Detector</h2>

{cameraError ? (
<div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">
<strong className="font-bold">Camera Error:</strong>
<span className="block sm:inline"> {cameraError}</span>
</div>
) : (
<div className="mb-4 rounded-lg overflow-hidden shadow-lg border-2 border-gray-300 bg-gray-100 min-h-[300px] relative">
{!imgSrc ? (
<Webcam
audio={false}
ref={webcamRef}
screenshotFormat="image/jpeg"
className="w-full h-full object-cover"
onUserMediaError={(err) => setCameraError("Could not access camera. Please check permissions.")}
/>
) : (
<div className="relative">
<img src={imgSrc} alt="Captured" className="w-full" />
{/* Since CLIP doesn't give boxes, we just show a banner if detected */}
{detections.length > 0 && (
<div className="absolute top-0 left-0 right-0 bg-red-600 text-white p-2 text-center font-bold opacity-90">
DETECTED: {detections.map(d => d.label).join(', ')}
</div>
)}
</div>
)}
</div>
)}

<div className="flex justify-center gap-4">
{!imgSrc ? (
<button
onClick={capture}
disabled={!!cameraError}
className={`bg-blue-600 text-white px-6 py-2 rounded-full font-semibold shadow-md hover:bg-blue-700 transition ${cameraError ? 'opacity-50 cursor-not-allowed' : ''}`}
>
Capture Photo
</button>
) : (
<>
<button
onClick={retake}
className="bg-gray-500 text-white px-6 py-2 rounded-full font-semibold shadow-md hover:bg-gray-600 transition"
>
Retake
</button>
<button
onClick={detectTreeHazard}
disabled={loading}
className={`bg-green-600 text-white px-6 py-2 rounded-full font-semibold shadow-md hover:bg-green-700 transition flex items-center ${loading ? 'opacity-70 cursor-wait' : ''}`}
>
{loading ? (
<>
<svg className="animate-spin -ml-1 mr-2 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Analyzing...
</>
) : 'Detect Hazard'}
</button>
</>
)}
</div>

<p className="mt-4 text-sm text-gray-600 text-center">
Point camera at fallen trees or dangerous branches.
</p>
</div>
);
};

export default TreeDetector;
12 changes: 11 additions & 1 deletion frontend/src/views/Home.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { AlertTriangle, MapPin, Search, Activity, Camera, Trash2, ThumbsUp, Brush, Droplets, Zap, Truck, Flame, Dog, XCircle, Lightbulb } from 'lucide-react';
import { AlertTriangle, MapPin, Search, Activity, Camera, Trash2, ThumbsUp, Brush, Droplets, Zap, Truck, Flame, Dog, XCircle, Lightbulb, TreePine } from 'lucide-react';

const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote }) => (
<div className="space-y-6">
Expand Down Expand Up @@ -125,6 +125,16 @@ const Home = ({ setView, fetchResponsibilityMap, recentIssues, handleUpvote }) =
</div>
<span className="font-semibold text-gray-800 text-sm">Blocked Road</span>
</button>

<button
onClick={() => setView('tree')}
className="flex flex-col items-center justify-center bg-green-50 border-2 border-green-100 p-4 rounded-xl hover:bg-green-100 transition shadow-sm h-32"
>
<div className="bg-green-700 text-white p-3 rounded-full mb-2">
<TreePine size={24} />
</div>
<span className="font-semibold text-green-900 text-sm">Tree Hazard</span>
</button>
</div>

<div className="grid grid-cols-1 mt-4">
Expand Down
24 changes: 5 additions & 19 deletions netlify.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,15 @@
# Netlify Configuration for VishwaGuru Frontend

# Build settings
[build]
base = "frontend"
publish = "dist"
command = "npm run build"

# Build environment
[build.environment]
NODE_VERSION = "20"

# Environment variables (set these in Netlify dashboard)
# VITE_API_URL = https://your-backend.onrender.com

# Redirects for SPA
[[redirects]]
from = "/*"
to = "/index.html"
status = 200

# Headers for security
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"
[[redirects]]
from = "/api/*"
to = "https://vishwaguru-backend.onrender.com/api/:splat"
status = 200
force = true
Copy link

Copilot AI Jan 13, 2026

Choose a reason for hiding this comment

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

The removal of security headers (X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, and Referrer-Policy) reduces the application's security posture. These headers provide important protection against clickjacking, MIME-type sniffing, XSS attacks, and referrer leakage. Consider keeping these security headers to maintain best security practices for the frontend deployment.

Suggested change
force = true
force = true
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
X-XSS-Protection = "1; mode=block"
Referrer-Policy = "strict-origin-when-cross-origin"

Copilot uses AI. Check for mistakes.
Loading
Loading