File size: 20,052 Bytes
bef097a
 
 
 
 
 
fc163de
bef097a
 
fed1ce6
 
 
 
7eb52be
2875890
29c6836
bef097a
2875890
bef097a
fed1ce6
bef097a
 
 
 
 
 
 
 
96ad65a
bef097a
 
96ad65a
bef097a
 
6a21db2
bef097a
 
00457fc
bef097a
96ad65a
 
 
 
 
 
bef097a
 
 
 
865b4b9
bef097a
865b4b9
bef097a
865b4b9
bef097a
865b4b9
bef097a
 
 
 
 
 
 
 
cbb8b23
f240301
 
eeb1a77
11a0e5f
cbb8b23
f240301
6a21db2
f240301
cbc9f66
f240301
 
 
 
865e5ad
f240301
 
6a21db2
2891bd6
48c3317
f240301
6a21db2
f240301
 
 
48c3317
f240301
 
 
6a21db2
f240301
6a21db2
865e5ad
5619a6d
94e3af6
 
 
51ebf6e
f240301
6a21db2
f240301
 
 
0800129
f240301
764ea7f
 
0673439
764ea7f
0673439
 
 
 
764ea7f
0673439
764ea7f
00457fc
 
 
 
 
fc163de
 
 
aaf6140
 
 
 
 
 
 
fc163de
 
 
882b73b
 
 
 
 
df0ebd8
 
 
0b95e11
df0ebd8
 
882b73b
 
 
7eb52be
 
 
 
 
2875890
 
 
 
5127585
33c271e
 
 
 
5127585
 
 
 
 
 
 
 
 
 
7eb52be
 
 
4f95521
 
 
e8b090c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4f95521
 
882b73b
00457fc
 
 
 
 
 
 
 
 
767e866
00457fc
 
 
 
 
 
 
 
 
 
 
 
 
2891bd6
 
 
 
 
 
 
 
 
58d7125
2891bd6
 
 
 
49624d5
764ea7f
2891bd6
 
764ea7f
2891bd6
00457fc
 
 
 
cbb8b23
 
00457fc
 
c88fc82
00457fc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5f8b621
00457fc
29c6836
a067e03
b8220a5
00457fc
 
 
 
 
 
 
 
a3a7660
00457fc
 
29c6836
cbb8b23
 
00457fc
db93a8f
00457fc
 
 
 
 
 
ce33c2b
 
6a21db2
 
 
 
ce33c2b
 
 
 
6a21db2
 
 
 
8335916
 
 
 
 
 
 
 
 
 
 
 
 
 
939e172
f240301
cbb8b23
f240301
 
41ef522
f240301
262166a
 
bd8261f
 
 
 
c706c8f
41d7a51
c423934
bd8261f
11a0e5f
bd8261f
 
 
c423934
9fe1062
cbb8b23
f240301
 
 
 
 
 
7dedd16
7a91f9b
7dedd16
 
7a91f9b
8335916
 
 
 
7dedd16
8335916
7dedd16
8335916
a4728a9
 
 
03c9a65
 
e1f1397
03c9a65
 
6a21db2
03c9a65
7dedd16
a4728a9
fc163de
767e866
 
 
 
 
 
 
8335916
 
 
 
 
7dedd16
8335916
96ad65a
 
 
58d7125
7dedd16
 
 
ebe7da2
 
 
 
 
 
4f7b289
 
 
 
 
 
 
 
 
 
ebe7da2
 
bef097a
 
ebe7da2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
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")