pothole-backend / app.py
sunilchm's picture
get extension of video
cbc9f66
Raw
History Blame Contribute Delete
20.1 kB
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 ---
@app.get("/")
async def root():
return {"message": "API is running"}
@app.get("/detailed_report_data")
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)}")
@app.get("/get_pothole_reports")
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
@app.get("/get_today_processed_reports")
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)}")
@app.get("/get_total_processed_reports")
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)}")
@app.get("/report_summary")
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)}")
@app.get("/latest_reports")
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)}")
@app.post("/upload")
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}
@app.patch("/report/{guid}/status")
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)}")
@app.patch("/frame/{frame_id}/status")
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)}")
@app.post("/upload_video")
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}
@app.get("/video_report/{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)}")
@app.get("/frame/{frame_id}")
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)}")
@app.get("/videos")
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)}")
@app.get("/images")
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)}")
@app.get("/reports")
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)}")
@app.get("/report/{guid}")
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)}")
@app.get("/get_billboard_stats")
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)}")
@app.get("/get_pothole_stats")
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")