Spaces:
Sleeping
Sleeping
| from supabase import create_client, Client | |
| import os | |
| import uuid | |
| from fractions import Fraction | |
| import exifread | |
| import uvicorn | |
| from fastapi import FastAPI, UploadFile, File, HTTPException, Form, BackgroundTasks, Body, Query | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from dotenv import load_dotenv | |
| from ultralytics import YOLO | |
| import cv2 | |
| import numpy as np | |
| import tempfile | |
| from datetime import datetime, date, timedelta, timezone | |
| import logging | |
| import random | |
| logger = logging.getLogger("uvicorn.error") | |
| load_dotenv() | |
| model = YOLO('best.pt') | |
| app = FastAPI() | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"] | |
| ) | |
| SUPABASE_URL = os.getenv("SUPABASE_URL") | |
| SUPABASE_KEY = os.getenv("SUPABASE_KEY") | |
| if not SUPABASE_URL or not SUPABASE_KEY: | |
| raise ValueError("Supabase credentials not found.") | |
| supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY) | |
| # --- Helper Functions --- | |
| def convert_to_decimal(coord): | |
| if coord and len(coord.values) >= 3: | |
| deg = float(coord.values[0]) | |
| min = float(coord.values[1]) / 60 | |
| sec = float(Fraction(str(coord.values[2]))) / 3600 | |
| return deg + min + sec | |
| return 0.0 | |
| def get_gps_location(file_stream): | |
| tags = exifread.process_file(file_stream) | |
| gps_latitude = tags.get("GPS GPSLatitude") | |
| print(f"GPS GPSLatitude: {gps_latitude}") | |
| gps_latitude_ref = tags.get("GPS GPSLatitudeRef") | |
| print(f"GPS GPSLatitudeRef: {gps_latitude_ref}") | |
| gps_longitude = tags.get("GPS GPSLongitude") | |
| print(f"GPS GPSLongitude: {gps_longitude}") | |
| gps_longitude_ref = tags.get("GPS GPSLongitudeRef") | |
| print(f"GPS GPSLongitudeRef: {gps_longitude_ref}") | |
| if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref: | |
| lat = convert_to_decimal(gps_latitude) | |
| lon = convert_to_decimal(gps_longitude) | |
| if gps_latitude_ref.values[0] != 'N': lat = -lat | |
| if gps_longitude_ref.values[0] != 'E': lon = -lon | |
| return lat, lon | |
| return None, None | |
| def process_video_in_background(video_data: bytes, video_guid: str, filename: str, location_point: str , location: str , is_approved: int , getvariancevalue: str, category: str, city: str): | |
| bucket_name = 'pothole-images' | |
| try: | |
| supabase.table("videos").insert({"guid": video_guid, "video_name": filename, "status": "processing", | |
| "location": location_point, "location_text": location, | |
| "category": category, "approved": is_approved, "variance": getvariancevalue, "city": city}).execute() | |
| except Exception as e: | |
| print(f"DB Error on video insert: {str(e)}") | |
| return | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as temp_video_file: | |
| temp_video_file.write(video_data) | |
| temp_path = temp_video_file.name | |
| cap = cv2.VideoCapture(temp_path) | |
| frame_number = 0 | |
| first_frame_saved = False | |
| while cap.isOpened(): | |
| ret, frame = cap.read() | |
| if not ret: break | |
| if frame_number % 90 == 0: | |
| results = model(frame, device='cpu') | |
| if len(results[0].boxes) > 0: | |
| annotated_frame = results[0].plot() | |
| is_success, buffer = cv2.imencode(".jpg", annotated_frame) | |
| if is_success: | |
| first_frame_set = {} | |
| frame_bytes = buffer.tobytes() | |
| frame_path = f"{video_guid}/frame_{frame_number}.jpg" | |
| try: | |
| supabase.storage.from_(bucket_name).upload(path=frame_path, file=frame_bytes, file_options={"content-type": "image/jpeg"}) | |
| frame_url = supabase.storage.from_(bucket_name).get_public_url(frame_path) | |
| supabase.table("detected_frames").insert({"video_guid": video_guid, "frame_image_url": frame_url, "frame_number": frame_number, "status": "In Progress"}).execute() | |
| if not first_frame_saved: | |
| supabase.table("videos").update({"image_url": frame_url | |
| }).eq("guid", video_guid).execute() | |
| first_frame_saved = True | |
| except Exception as e: | |
| print(f"Error saving frame {frame_number}: {str(e)}") | |
| frame_number += 1 | |
| cap.release() | |
| os.remove(temp_path) | |
| supabase.table("videos").update({"status": "complete"}).eq("guid", video_guid).execute() | |
| def safe_date_converter(date_string): | |
| if not date_string: | |
| return datetime.min.replace(tzinfo=timezone.utc) | |
| try: | |
| dt = datetime.fromisoformat(date_string) | |
| if dt.tzinfo is None: | |
| return dt.replace(tzinfo=timezone.utc) | |
| return dt | |
| except (ValueError, TypeError): | |
| return datetime.min.replace(tzinfo=timezone.utc) | |
| # --- API Endpoints --- | |
| async def root(): | |
| return {"message": "API is running"} | |
| async def get_detailed_report_data(category: str = Query(...)): | |
| try: | |
| # Fetch potholes reports for the specified category | |
| response_potholes = supabase.table("potholes").select("*").eq("category", category).order("created_at", desc=True).execute() | |
| # Fetch videos reports for the specified category | |
| response_videos = supabase.table("videos").select("*").eq("category", category).order("created_at", desc=True).execute() | |
| return response_potholes.data + response_videos.data | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| async def get_pothole_reports(): | |
| try: | |
| # Fetch all pothole reports | |
| reports_potholes_response = supabase.table("potholes").select("*").eq("category", "potholes").execute() | |
| # Fetch all videos reports | |
| reports_videos_response = supabase.table("videos").select("*").eq("category", "potholes").execute() | |
| return reports_potholes_response.data + reports_videos_response.data | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| # Today's date range in UTC | |
| async def get_today_processed_reports(): | |
| try: | |
| today = date.today() | |
| start_of_day = datetime.combine(today, datetime.min.time()).replace(tzinfo=timezone.utc) | |
| end_of_day = start_of_day + timedelta(days=1) | |
| # Fetch all pothole reports | |
| reports_response = supabase.table("potholes") \ | |
| .select("*") \ | |
| .gte("created_at", start_of_day.isoformat()) \ | |
| .lt("created_at", end_of_day.isoformat()) \ | |
| .execute() | |
| # Fetch all videos reports | |
| reports_videos_response = supabase.table("videos") \ | |
| .select("*") \ | |
| .gte("created_at", start_of_day.isoformat()) \ | |
| .lt("created_at", end_of_day.isoformat()) \ | |
| .execute() | |
| return reports_response.data + reports_videos_response.data | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| async def get_total_processed_reports(): | |
| try: | |
| reports_potholes_response = supabase.table("potholes").select("*").order("created_at", desc=True).execute() | |
| reports_videos_response = supabase.table("videos").select("*").order("created_at", desc=True).execute() | |
| print(f"Total reports potholes fetched: {len(reports_potholes_response.data)}") | |
| print(f"Total reports videos fetched: {len(reports_videos_response.data)}") | |
| print(f"potholes Data : {reports_potholes_response.data}") | |
| print(f"Videos Data : {reports_videos_response.data}") | |
| # Extract data | |
| potholes_data = reports_potholes_response.data or [] | |
| videos_data = reports_videos_response.data or [] | |
| # Combine and sort by created_at | |
| all_data = potholes_data + videos_data | |
| all_data_sorted = sorted(all_data, key=lambda x: datetime.fromisoformat(x["created_at"].replace("Z", "")), reverse=True) | |
| # Take latest 4 records | |
| latest_4 = all_data_sorted[:4] | |
| return latest_4 | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| async def get_report_summary(): | |
| try: | |
| reports_response = supabase.table("potholes").select("category, approved").execute() | |
| all_reports = reports_response.data | |
| summary = { | |
| "billboard": {"total": 0, "approved": 0, "unapproved": 0, "damage": 0}, | |
| "guardrails": {"total": 0, "missing": 0, "damaged": 0}, | |
| "construction": {"total": 0}, | |
| "potholes": {"total": 0, "approved": 0, "unapproved": 0} | |
| } | |
| for report in all_reports: | |
| category = report.get("category") | |
| is_approved = report.get("approved") | |
| if category in summary: | |
| summary[category]["total"] += 1 | |
| if is_approved == 1: | |
| summary[category]["approved"] += 1 | |
| summary[category]["unapproved"] = summary[category]["total"] - summary[category]["approved"] | |
| return summary | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") | |
| async def get_latest_reports(): | |
| try: | |
| videos_response = supabase.table("videos").select("guid, video_name, created_at, status").order("created_at", desc=True).limit(30).execute() | |
| videos = videos_response.data | |
| for video in videos: | |
| video['type'] = 'video' | |
| first_frame_response = supabase.table("detected_frames").select("frame_image_url").eq("video_guid", video["guid"]).order("frame_number").limit(1).execute() | |
| video["thumbnail_url"] = first_frame_response.data[0]["frame_image_url"] if first_frame_response.data else None | |
| images_response = supabase.table("potholes").select("guid, image_name, image_url, created_at, status, category").order("created_at", desc=True).limit(30).execute() | |
| images = images_response.data | |
| for image in images: | |
| image['type'] = 'image' | |
| image['thumbnail_url'] = image.get('image_url') | |
| all_reports = videos + images | |
| combined_reports = sorted(all_reports, key=lambda x: safe_date_converter(x.get('created_at')), reverse=True) | |
| return combined_reports[:30] | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"A severe database error occurred: {str(e)}") | |
| async def upload_image( | |
| file: UploadFile = File(...), | |
| location: str = Form(None), | |
| category: str = Form(...), | |
| city: str = Form(...) | |
| ): | |
| image_data = await file.read() | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg") as temp_image_file: | |
| temp_image_file.write(image_data) | |
| temp_path = temp_image_file.name | |
| results = model(temp_path, device='cpu') | |
| annotated_image_array = results[0].plot() | |
| os.remove(temp_path) | |
| is_success, buffer = cv2.imencode(".jpg", annotated_image_array) | |
| if not is_success: raise HTTPException(status_code=500, detail="Failed to encode annotated image.") | |
| annotated_image_bytes = buffer.tobytes() | |
| bucket_name = 'pothole-images' | |
| uuid_path = f"annotated_{uuid.uuid4()}.jpg" | |
| try: | |
| supabase.storage.from_(bucket_name).upload(path=uuid_path, file=annotated_image_bytes, file_options={"content-type": "image/jpeg"}) | |
| url = supabase.storage.from_(bucket_name).get_public_url(uuid_path) | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Storage Error: {str(e)}") | |
| await file.seek(0) | |
| lat, lon = get_gps_location(file.file) | |
| location_point = f"Point({lon:.6f} {lat:.6f})" if lat and lon else None | |
| guid = str(uuid.uuid4()) | |
| variance_levels = ["Low", "Mid", "High"] | |
| getvariancevalue = random.choice(variance_levels) if category == "potholes" else None | |
| try: | |
| validation_response = supabase.table("validation").select("location_text").execute() | |
| approved_locations = {item['location_text'] for item in validation_response.data} | |
| is_approved = 1 if location in approved_locations else 0 | |
| insert_data = { | |
| "guid": guid, | |
| "image_name": file.filename, | |
| "image_url": url, | |
| "location": location_point, | |
| "location_text": f"{lon:.6f} {lat:.6f}" if lat and lon else location or None, | |
| "status": "In Progress", | |
| "category": category, | |
| "approved": is_approved, | |
| "variance": getvariancevalue, | |
| "city": city | |
| } | |
| response = supabase.table("potholes").insert(insert_data).execute() | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| return {"message": "Upload successful", "guid": guid, "image_url": url, "db_response": response.data} | |
| async def update_report_status(guid: str, payload: dict = Body(...)): | |
| new_status = payload.get("status") | |
| if not new_status: | |
| raise HTTPException(status_code=400, detail="Status not provided.") | |
| try: | |
| report_res = supabase.table("potholes").select("status").eq("guid", guid).single().execute() | |
| current_status = report_res.data.get("status") if report_res.data else None | |
| final_status = 'In Progress' if current_status == new_status else new_status | |
| response = supabase.table("potholes").update({"status": final_status}).eq("guid", guid).execute() | |
| return {"message": "Status updated successfully", "data": response.data} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def update_frame_status(frame_id: int, payload: dict = Body(...)): | |
| new_status = payload.get("status") | |
| if not new_status: | |
| raise HTTPException(status_code=400, detail="Status not provided.") | |
| try: | |
| frame_res = supabase.table("detected_frames").select("status").eq("id", frame_id).single().execute() | |
| current_status = frame_res.data.get("status") if frame_res.data else None | |
| final_status = 'In Progress' if current_status == new_status else new_status | |
| response = supabase.table("detected_frames").update({"status": final_status}).eq("id", frame_id).execute() | |
| return {"message": "Status updated successfully", "data": response.data} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def upload_video(background_tasks: BackgroundTasks, location: str = Form(None), category: str = Form(...), city: str = Form(...), file: UploadFile = File(...)): | |
| if not file.content_type.startswith('video/'): | |
| raise HTTPException(status_code=400, detail="File is not a video.") | |
| video_data = await file.read() | |
| video_guid = str(uuid.uuid4()) | |
| lat, lon = get_gps_location(file.file) | |
| location_point = f"Point({lon:.6f} {lat:.6f})" if lat and lon else None | |
| validation_response = supabase.table("validation").select("location_text").execute() | |
| approved_locations = {item['location_text'] for item in validation_response.data} | |
| is_approved = 1 if location in approved_locations else 0 | |
| variance_levels = ["Low", "Mid", "High"] | |
| getvariancevalue = random.choice(variance_levels) if category == "potholes" else None | |
| location =f"{lon:.6f} {lat:.6f}" if lat and lon else location or None | |
| print(f"Step1") | |
| print(f"location_point: {location_point}") | |
| print(f"location text: {location}") | |
| print(f"is approved : {is_approved}") | |
| print(f"variancevalue: {getvariancevalue}") | |
| print(f"category: {category}") | |
| print(f"Step2") | |
| print(f"city: {city}") | |
| background_tasks.add_task(process_video_in_background, video_data, video_guid, file.filename, location_point, location, is_approved, getvariancevalue, category, city) | |
| return {"message": "Video upload successful. Processing has started.", "video_guid": video_guid} | |
| async def get_video_report(guid: str): | |
| try: | |
| video_response = supabase.table("videos").select("*").eq("guid", guid).single().execute() | |
| if not video_response.data: raise HTTPException(status_code=404, detail="Video report not found") | |
| frames_response = supabase.table("detected_frames").select("*").eq("video_guid", guid).order("frame_number", desc=True).limit(15).execute() | |
| return { "video_info": video_response.data, "detected_frames": frames_response.data } | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_frame(frame_id: int): | |
| try: | |
| response = supabase.table("detected_frames").select("*, videos(*), status").eq("id", frame_id).single().execute() | |
| if not response.data: raise HTTPException(status_code=404, detail="Frame not found") | |
| return response.data | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_videos(): | |
| try: | |
| videos_response = supabase.table("videos").select("*").order("created_at", desc=True).limit(30).execute() | |
| videos = videos_response.data | |
| video_count = len(videos) | |
| for video in videos: | |
| first_frame_response = supabase.table("detected_frames").select("frame_image_url").eq("video_guid", video["guid"]).order("frame_number").limit(1).execute() | |
| video["thumbnail_url"] = first_frame_response.data[0]["frame_image_url"] if first_frame_response.data else None | |
| return videos | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_images(): | |
| try: | |
| response = supabase.table("potholes").select("id, guid, image_name, image_url, created_at, status, location_text").order("created_at", desc=True).execute() | |
| return response.data | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_reports(): | |
| try: | |
| response = supabase.table("potholes").select("id, guid, image_name, image_url, created_at, status").order("created_at", desc=True).limit(30).execute() | |
| return response.data | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_report(guid: str): | |
| try: | |
| response = supabase.table("potholes").select("guid, image_name, image_url, location, created_at, location_text, status, category").eq("guid", guid).single().execute() | |
| if not response.data: raise HTTPException(status_code=404, detail=f"Report with guid {guid} not found.") | |
| return response.data | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_billboard_stats(): | |
| try: | |
| # Call the RPC function | |
| response = supabase.rpc("get_billboard_stats").execute() | |
| print("Response : billboard stats:", response.data) | |
| return response.data | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| async def get_pothole_stats(): | |
| try: | |
| # Call the RPC function | |
| response = supabase.rpc("get_pothole_stats").execute() | |
| print("Response : pothole stats:", response.data) | |
| return response.data | |
| except Exception as e: raise HTTPException(status_code=500, detail=f"Database Error: {str(e)}") | |
| if __name__ == "__main__": | |
| uvicorn.run(app, port=8000, host="127.0.0.1") | |