Spaces:
Sleeping
Sleeping
Upload 8 files
Browse files- .gitattributes +1 -0
- app.py +569 -0
- models/3dcnn_accident_model.pth +3 -0
- models/ensemble_svm_model.pkl +3 -0
- models/traffic_predictor.pkl +3 -0
- models/yolov8_accident_model.pt +3 -0
- requirements.txt +15 -0
- static/logo.PNG +3 -0
- templates/index.html +1106 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
static/logo.PNG filter=lfs diff=lfs merge=lfs -text
|
app.py
ADDED
|
@@ -0,0 +1,569 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import io
|
| 3 |
+
import cv2
|
| 4 |
+
import base64
|
| 5 |
+
import torch
|
| 6 |
+
import torch.nn as nn
|
| 7 |
+
import numpy as np
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import joblib
|
| 10 |
+
import smtplib
|
| 11 |
+
import ssl
|
| 12 |
+
import threading
|
| 13 |
+
import uuid
|
| 14 |
+
import time
|
| 15 |
+
import requests
|
| 16 |
+
from urllib.parse import urlparse
|
| 17 |
+
from email.message import EmailMessage
|
| 18 |
+
from flask import Flask, request, render_template, jsonify, Response, send_from_directory
|
| 19 |
+
from werkzeug.utils import secure_filename
|
| 20 |
+
|
| 21 |
+
# --- Auto-install missing libraries ---
|
| 22 |
+
try:
|
| 23 |
+
from ultralytics import YOLO
|
| 24 |
+
import easyocr
|
| 25 |
+
except ModuleNotFoundError:
|
| 26 |
+
import sys
|
| 27 |
+
import subprocess
|
| 28 |
+
print("Installing ultralytics and easyocr (ALPR)... This might take a minute...")
|
| 29 |
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "ultralytics", "scikit-learn", "easyocr", "pandas", "requests"])
|
| 30 |
+
from ultralytics import YOLO
|
| 31 |
+
import easyocr
|
| 32 |
+
|
| 33 |
+
from torchvision.models.video import r3d_18
|
| 34 |
+
|
| 35 |
+
app = Flask(__name__)
|
| 36 |
+
|
| 37 |
+
# ==========================================
|
| 38 |
+
# 🚨 ALERT CONFIGURATION (EMAIL SETUP) 🚨
|
| 39 |
+
# ==========================================
|
| 40 |
+
ALERT_EMAIL_SENDER = "gowreeshgowri50@gmail.com"
|
| 41 |
+
ALERT_EMAIL_PASSWORD = "omzw fjsu nwnr sgvl"
|
| 42 |
+
ALERT_EMAIL_RECEIVER = "ridhinmr32@gmail.com"
|
| 43 |
+
ENABLE_EMAIL_ALERTS = True
|
| 44 |
+
|
| 45 |
+
# --- Configurations ---
|
| 46 |
+
UPLOAD_FOLDER = 'uploads'
|
| 47 |
+
MODEL_FOLDER = 'models'
|
| 48 |
+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
| 49 |
+
|
| 50 |
+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
|
| 51 |
+
os.makedirs(MODEL_FOLDER, exist_ok=True)
|
| 52 |
+
|
| 53 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 54 |
+
classes = ['major', 'minor', 'moderate']
|
| 55 |
+
|
| 56 |
+
# --- Load Models ---
|
| 57 |
+
print("Loading AI Models & ALPR... This might take a few seconds.")
|
| 58 |
+
models_loaded = False
|
| 59 |
+
try:
|
| 60 |
+
yolo_path = os.path.join(MODEL_FOLDER, 'yolov8_accident_model.pt')
|
| 61 |
+
model_yolo = YOLO(yolo_path)
|
| 62 |
+
|
| 63 |
+
cnn3d_path = os.path.join(MODEL_FOLDER, '3dcnn_accident_model.pth')
|
| 64 |
+
model_3d = r3d_18()
|
| 65 |
+
model_3d.fc = nn.Linear(model_3d.fc.in_features, 3)
|
| 66 |
+
model_3d.load_state_dict(torch.load(cnn3d_path, map_location=device))
|
| 67 |
+
model_3d.to(device)
|
| 68 |
+
model_3d.eval()
|
| 69 |
+
|
| 70 |
+
svm_path = os.path.join(MODEL_FOLDER, 'ensemble_svm_model.pkl')
|
| 71 |
+
model_svm = joblib.load(svm_path)
|
| 72 |
+
|
| 73 |
+
print("Loading OCR Engine...")
|
| 74 |
+
ocr_reader = easyocr.Reader(['en'], gpu=torch.cuda.is_available())
|
| 75 |
+
|
| 76 |
+
models_loaded = True
|
| 77 |
+
print("✅ Visual AI Models & OCR Loaded Successfully!")
|
| 78 |
+
except Exception as e:
|
| 79 |
+
print(f"⚠️ Warning: Could not load real visual models. Using mock simulation. Error: {e}")
|
| 80 |
+
|
| 81 |
+
# Load Traffic Predictor (Tabular Model)
|
| 82 |
+
try:
|
| 83 |
+
traffic_model_path = os.path.join(MODEL_FOLDER, 'traffic_predictor.pkl')
|
| 84 |
+
model_traffic = joblib.load(traffic_model_path)
|
| 85 |
+
print("✅ Traffic Risk Predictor Loaded Successfully!")
|
| 86 |
+
except Exception as e:
|
| 87 |
+
print(f"⚠️ Warning: Could not load traffic predictor model. Error: {e}")
|
| 88 |
+
model_traffic = None
|
| 89 |
+
|
| 90 |
+
def cleanup_old_files():
|
| 91 |
+
for f in os.listdir(app.config['UPLOAD_FOLDER']):
|
| 92 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], f)
|
| 93 |
+
try:
|
| 94 |
+
if os.path.isfile(file_path):
|
| 95 |
+
if time.time() - os.path.getmtime(file_path) > 3600:
|
| 96 |
+
os.remove(file_path)
|
| 97 |
+
except Exception as e: pass
|
| 98 |
+
|
| 99 |
+
# --- NEW: Extract Location and Weather from IP Camera URL ---
|
| 100 |
+
def get_camera_info_from_ip(url):
|
| 101 |
+
try:
|
| 102 |
+
parsed = urlparse(url)
|
| 103 |
+
netloc = parsed.netloc.split(':')[0]
|
| 104 |
+
if not netloc: return None, None
|
| 105 |
+
|
| 106 |
+
print(f"🔍 Tracing IP Address: {netloc}...")
|
| 107 |
+
|
| 108 |
+
# 1. Get City/Country from IP Address
|
| 109 |
+
res = requests.get(f"http://ip-api.com/json/{netloc}", timeout=5).json()
|
| 110 |
+
if res.get("status") == "success":
|
| 111 |
+
city = res.get("city", "Unknown City")
|
| 112 |
+
country = res.get("countryCode", "Unknown Country")
|
| 113 |
+
lat = res.get("lat")
|
| 114 |
+
lon = res.get("lon")
|
| 115 |
+
cam_location = f"{city}, {country}"
|
| 116 |
+
print(f"🌍 IP Geolocation Success! Camera is located in: {cam_location}")
|
| 117 |
+
|
| 118 |
+
# 2. Get Real-time Weather for that Camera's Location
|
| 119 |
+
try:
|
| 120 |
+
wx_res = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}¤t_weather=true", timeout=5).json()
|
| 121 |
+
temp = wx_res["current_weather"]["temperature"]
|
| 122 |
+
cam_weather = f"{temp}°C, Active"
|
| 123 |
+
print(f"⛅ Weather API Success! Conditions: {cam_weather}")
|
| 124 |
+
except Exception as wx_e:
|
| 125 |
+
print(f"⚠️ Weather API failed: {wx_e}")
|
| 126 |
+
cam_weather = "--"
|
| 127 |
+
|
| 128 |
+
return cam_location, cam_weather
|
| 129 |
+
else:
|
| 130 |
+
print(f"❌ IP API Error: {res.get('message', 'Unknown Error')}")
|
| 131 |
+
except Exception as e:
|
| 132 |
+
print(f"❌ IP Geolocation failed: {e}")
|
| 133 |
+
return None, None
|
| 134 |
+
|
| 135 |
+
def send_email_alert(location, confidence, plates_data, image_b64, severity, video_path=None):
|
| 136 |
+
if not ENABLE_EMAIL_ALERTS:
|
| 137 |
+
return
|
| 138 |
+
try:
|
| 139 |
+
msg = EmailMessage()
|
| 140 |
+
msg['Subject'] = f"🚨 {severity.upper()} COLLISION DETECTED - {location}"
|
| 141 |
+
msg['From'] = ALERT_EMAIL_SENDER
|
| 142 |
+
msg['To'] = ALERT_EMAIL_RECEIVER
|
| 143 |
+
|
| 144 |
+
# Extract just the text from the plates dictionary for the email body
|
| 145 |
+
plates_text_list = [p['text'] for p in plates_data] if plates_data else []
|
| 146 |
+
plates_text = ', '.join(plates_text_list) if plates_text_list else 'None Detected'
|
| 147 |
+
|
| 148 |
+
content = f"""
|
| 149 |
+
EMERGENCY DISPATCH ALERT
|
| 150 |
+
------------------------
|
| 151 |
+
A collision has been detected by CrashVision AI.
|
| 152 |
+
|
| 153 |
+
Location: {location}
|
| 154 |
+
Severity: {severity.upper()} COLLISION
|
| 155 |
+
AI Confidence: {confidence}%
|
| 156 |
+
Detected Plates: {plates_text}
|
| 157 |
+
|
| 158 |
+
Immediate response requested. See attached surveillance media.
|
| 159 |
+
"""
|
| 160 |
+
msg.set_content(content)
|
| 161 |
+
|
| 162 |
+
if image_b64:
|
| 163 |
+
img_data = base64.b64decode(image_b64)
|
| 164 |
+
msg.add_attachment(img_data, maintype='image', subtype='jpeg', filename='incident_snapshot.jpg')
|
| 165 |
+
|
| 166 |
+
if video_path and os.path.exists(video_path):
|
| 167 |
+
if video_path.lower().endswith(('.mp4', '.avi', '.mov', '.webm')):
|
| 168 |
+
file_size = os.path.getsize(video_path)
|
| 169 |
+
if file_size < 20 * 1024 * 1024:
|
| 170 |
+
with open(video_path, 'rb') as f:
|
| 171 |
+
vid_data = f.read()
|
| 172 |
+
msg.add_attachment(vid_data, maintype='video', subtype='mp4', filename='incident_video.mp4')
|
| 173 |
+
|
| 174 |
+
context = ssl.create_default_context()
|
| 175 |
+
with smtplib.SMTP_SSL('smtp.gmail.com', 465, context=context) as smtp:
|
| 176 |
+
smtp.login(ALERT_EMAIL_SENDER, ALERT_EMAIL_PASSWORD)
|
| 177 |
+
smtp.send_message(msg)
|
| 178 |
+
print("✅ Email Alert successfully sent!")
|
| 179 |
+
except Exception as e:
|
| 180 |
+
print(f"❌ Failed to send email alert: {e}")
|
| 181 |
+
|
| 182 |
+
def process_video_or_image(file_path):
|
| 183 |
+
is_image = file_path.lower().endswith(('.png', '.jpg', '.jpeg'))
|
| 184 |
+
frames_3d = []
|
| 185 |
+
yolo_probs = []
|
| 186 |
+
annotated_frame = None
|
| 187 |
+
best_raw_frame = None
|
| 188 |
+
|
| 189 |
+
if is_image:
|
| 190 |
+
frame = cv2.imread(file_path)
|
| 191 |
+
best_raw_frame = frame.copy()
|
| 192 |
+
res = model_yolo(file_path, verbose=False)[0]
|
| 193 |
+
annotated_frame = res.plot()
|
| 194 |
+
if res.probs is not None:
|
| 195 |
+
yolo_probs.append(res.probs.data.cpu().numpy())
|
| 196 |
+
elif res.boxes is not None and len(res.boxes) > 0:
|
| 197 |
+
confs = np.zeros(4) # SVM expects 4 classes from YOLO to total 15 features
|
| 198 |
+
for box in res.boxes:
|
| 199 |
+
cls_id = int(box.cls[0].item())
|
| 200 |
+
conf = box.conf[0].item()
|
| 201 |
+
if cls_id < 4 and conf > confs[cls_id]:
|
| 202 |
+
confs[cls_id] = conf
|
| 203 |
+
yolo_probs.append(confs)
|
| 204 |
+
f_3d = cv2.resize(frame, (112, 112))
|
| 205 |
+
f_3d = cv2.cvtColor(f_3d, cv2.COLOR_BGR2RGB)
|
| 206 |
+
frames_3d = [f_3d] * 16
|
| 207 |
+
else:
|
| 208 |
+
cap = cv2.VideoCapture(file_path)
|
| 209 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 210 |
+
if frame_count <= 0: return None, None, None, None
|
| 211 |
+
|
| 212 |
+
start_3d = int(frame_count * 0.70)
|
| 213 |
+
intervals_3d = np.linspace(start_3d, max(start_3d, frame_count-1), 16, dtype=int)
|
| 214 |
+
start_yolo = int(frame_count * 0.75)
|
| 215 |
+
intervals_yolo = np.linspace(start_yolo, max(start_yolo, frame_count-1), 5, dtype=int)
|
| 216 |
+
|
| 217 |
+
for idx in set(intervals_3d).union(set(intervals_yolo)):
|
| 218 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
|
| 219 |
+
ret, frame = cap.read()
|
| 220 |
+
if not ret: continue
|
| 221 |
+
if idx in intervals_3d:
|
| 222 |
+
f_3d = cv2.resize(frame, (112, 112))
|
| 223 |
+
f_3d = cv2.cvtColor(f_3d, cv2.COLOR_BGR2RGB)
|
| 224 |
+
frames_3d.append(f_3d)
|
| 225 |
+
if idx in intervals_yolo:
|
| 226 |
+
temp_path = os.path.join(UPLOAD_FOLDER, "temp_yolo_frame.jpg")
|
| 227 |
+
cv2.imwrite(temp_path, frame)
|
| 228 |
+
res = model_yolo(temp_path, verbose=False)[0]
|
| 229 |
+
best_raw_frame = frame.copy()
|
| 230 |
+
annotated_frame = res.plot()
|
| 231 |
+
if res.probs is not None:
|
| 232 |
+
yolo_probs.append(res.probs.data.cpu().numpy())
|
| 233 |
+
elif res.boxes is not None and len(res.boxes) > 0:
|
| 234 |
+
confs = np.zeros(4) # Parse bounding boxes for 4 classes
|
| 235 |
+
for box in res.boxes:
|
| 236 |
+
cls_id = int(box.cls[0].item())
|
| 237 |
+
conf = box.conf[0].item()
|
| 238 |
+
if cls_id < 4 and conf > confs[cls_id]:
|
| 239 |
+
confs[cls_id] = conf
|
| 240 |
+
yolo_probs.append(confs)
|
| 241 |
+
cap.release()
|
| 242 |
+
|
| 243 |
+
return frames_3d, yolo_probs, annotated_frame, best_raw_frame
|
| 244 |
+
|
| 245 |
+
def get_real_prediction(file_path, location_data):
|
| 246 |
+
frames_3d, yolo_probs, annotated_frame, best_raw_frame = process_video_or_image(file_path)
|
| 247 |
+
if not frames_3d: return mock_predict(location_data, file_path)
|
| 248 |
+
|
| 249 |
+
# --- 3D-CNN ---
|
| 250 |
+
if len(frames_3d) == 16:
|
| 251 |
+
tensor_3d = torch.tensor(np.array(frames_3d), dtype=torch.float32).permute(3, 0, 1, 2) / 255.0
|
| 252 |
+
tensor_3d = tensor_3d.unsqueeze(0).to(device)
|
| 253 |
+
with torch.no_grad():
|
| 254 |
+
out_3d = model_3d(tensor_3d)
|
| 255 |
+
prob_3d = torch.nn.functional.softmax(out_3d, dim=1).cpu().numpy()[0]
|
| 256 |
+
else:
|
| 257 |
+
prob_3d = np.array([0.33, 0.33, 0.33])
|
| 258 |
+
|
| 259 |
+
# --- YOLOv8 ---
|
| 260 |
+
if len(yolo_probs) > 0:
|
| 261 |
+
prob_mean = np.mean(yolo_probs, axis=0)
|
| 262 |
+
prob_max = np.max(yolo_probs, axis=0)
|
| 263 |
+
prob_min = np.min(yolo_probs, axis=0)
|
| 264 |
+
|
| 265 |
+
# Ensure length 4 for SVM compatibility (15 features total)
|
| 266 |
+
if len(prob_mean) < 4:
|
| 267 |
+
prob_mean = np.pad(prob_mean, (0, 4 - len(prob_mean)))
|
| 268 |
+
prob_max = np.pad(prob_max, (0, 4 - len(prob_max)))
|
| 269 |
+
prob_min = np.pad(prob_min, (0, 4 - len(prob_min)))
|
| 270 |
+
elif len(prob_mean) > 4:
|
| 271 |
+
prob_mean = prob_mean[:4]
|
| 272 |
+
prob_max = prob_max[:4]
|
| 273 |
+
prob_min = prob_min[:4]
|
| 274 |
+
else:
|
| 275 |
+
# Fallback to zeros of length 4 to prevent the 12-feature crash
|
| 276 |
+
prob_mean = prob_max = prob_min = np.zeros(4)
|
| 277 |
+
|
| 278 |
+
combined_features = np.concatenate((prob_mean, prob_max, prob_min, prob_3d)).reshape(1, -1)
|
| 279 |
+
ensemble_probs = model_svm.predict_proba(combined_features)[0]
|
| 280 |
+
final_pred_idx = model_svm.predict(combined_features)[0]
|
| 281 |
+
|
| 282 |
+
severity_label = classes[final_pred_idx]
|
| 283 |
+
confidence = ensemble_probs[final_pred_idx] * 100
|
| 284 |
+
|
| 285 |
+
# --- Fallback: If SVM doesn't trigger but YOLO sees an accident ---
|
| 286 |
+
yolo_max_conf = float(np.max(prob_max)) * 100
|
| 287 |
+
if confidence < 60 and yolo_max_conf > 60:
|
| 288 |
+
confidence = yolo_max_conf
|
| 289 |
+
severity_label = classes[min(int(np.argmax(prob_max)), 2)]
|
| 290 |
+
|
| 291 |
+
# --- ADVANCED ALPR: CROP AND EXTRACT PLATES ---
|
| 292 |
+
detected_plates = []
|
| 293 |
+
if best_raw_frame is not None:
|
| 294 |
+
ocr_results = ocr_reader.readtext(best_raw_frame, detail=1)
|
| 295 |
+
for (bbox, text, prob) in ocr_results:
|
| 296 |
+
text_clean = text.upper().strip()
|
| 297 |
+
if len(text_clean) > 4 and any(c.isalpha() for c in text_clean) and any(c.isdigit() for c in text_clean):
|
| 298 |
+
try:
|
| 299 |
+
x_min = max(0, int(min([p[0] for p in bbox])))
|
| 300 |
+
x_max = min(best_raw_frame.shape[1], int(max([p[0] for p in bbox])))
|
| 301 |
+
y_min = max(0, int(min([p[1] for p in bbox])))
|
| 302 |
+
y_max = min(best_raw_frame.shape[0], int(max([p[1] for p in bbox])))
|
| 303 |
+
|
| 304 |
+
plate_crop = best_raw_frame[y_min:y_max, x_min:x_max]
|
| 305 |
+
_, buffer = cv2.imencode('.jpg', plate_crop)
|
| 306 |
+
plate_b64 = base64.b64encode(buffer).decode('utf-8')
|
| 307 |
+
|
| 308 |
+
detected_plates.append({"text": text_clean, "image": plate_b64})
|
| 309 |
+
except Exception as e:
|
| 310 |
+
print(f"Error cropping plate: {e}")
|
| 311 |
+
detected_plates.append({"text": text_clean, "image": ""})
|
| 312 |
+
|
| 313 |
+
encoded_img = ""
|
| 314 |
+
if annotated_frame is not None:
|
| 315 |
+
_, buffer = cv2.imencode('.jpg', annotated_frame)
|
| 316 |
+
encoded_img = base64.b64encode(buffer).decode('utf-8')
|
| 317 |
+
|
| 318 |
+
alert_sent = True
|
| 319 |
+
print(f"\n{'='*50}\n🚨 {severity_label.upper()} ALERT DISPATCHED! 🚨")
|
| 320 |
+
print(f"📍 Location Triggered: {location_data}")
|
| 321 |
+
threading.Thread(target=send_email_alert, args=(location_data, confidence, detected_plates, encoded_img, severity_label, file_path)).start()
|
| 322 |
+
|
| 323 |
+
return {
|
| 324 |
+
"label": f"Accident Detected ({severity_label.capitalize()})",
|
| 325 |
+
"confidence": round(float(confidence), 1),
|
| 326 |
+
"cnn": round(float(np.max(prob_max)) * 100, 1),
|
| 327 |
+
"rcnn": round(float(np.max(prob_3d)) * 100, 1),
|
| 328 |
+
"alert_sent": alert_sent,
|
| 329 |
+
"plates": detected_plates,
|
| 330 |
+
"image": encoded_img
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
def mock_predict(location_data, file_path=None):
|
| 334 |
+
import random
|
| 335 |
+
svm_prob = np.random.uniform(0.7, 0.95)
|
| 336 |
+
cnn_prob = np.random.uniform(0.6, 0.95)
|
| 337 |
+
rcnn_prob = np.random.uniform(0.6, 0.9)
|
| 338 |
+
ensemble_prob = (svm_prob + cnn_prob + rcnn_prob) / 3
|
| 339 |
+
|
| 340 |
+
severities = ["Major", "Moderate", "Minor"]
|
| 341 |
+
severity_label = random.choice(severities)
|
| 342 |
+
|
| 343 |
+
alert_sent = True
|
| 344 |
+
plates = []
|
| 345 |
+
|
| 346 |
+
print(f"\n🚨 [MOCK] {severity_label.upper()} ALERT DISPATCHED! 🚨\n")
|
| 347 |
+
print(f"📍 Location Triggered: {location_data}")
|
| 348 |
+
threading.Thread(target=send_email_alert, args=(location_data, round(float(ensemble_prob) * 100, 1), plates, None, severity_label, file_path)).start()
|
| 349 |
+
|
| 350 |
+
return {
|
| 351 |
+
"label": f"Accident Detected ({severity_label})",
|
| 352 |
+
"confidence": round(float(ensemble_prob) * 100, 1),
|
| 353 |
+
"cnn": round(cnn_prob * 100, 1),
|
| 354 |
+
"rcnn": round(rcnn_prob * 100, 1),
|
| 355 |
+
"alert_sent": alert_sent,
|
| 356 |
+
"plates": plates,
|
| 357 |
+
"image": ""
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
# --- ROUTES ---
|
| 361 |
+
|
| 362 |
+
@app.route('/')
|
| 363 |
+
def index():
|
| 364 |
+
return render_template('index.html')
|
| 365 |
+
|
| 366 |
+
@app.route('/upload_media', methods=['POST'])
|
| 367 |
+
def upload_media():
|
| 368 |
+
"""Immediately saves file and returns ID so frontend can start video tracking."""
|
| 369 |
+
if 'file' not in request.files:
|
| 370 |
+
return jsonify({"error": "No file uploaded"}), 400
|
| 371 |
+
|
| 372 |
+
file = request.files['file']
|
| 373 |
+
if file.filename == '':
|
| 374 |
+
return jsonify({"error": "No selected file"}), 400
|
| 375 |
+
|
| 376 |
+
cleanup_old_files()
|
| 377 |
+
unique_id = f"{uuid.uuid4().hex}_{secure_filename(file.filename)}"
|
| 378 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_id)
|
| 379 |
+
file.save(file_path)
|
| 380 |
+
|
| 381 |
+
return jsonify({"video_id": unique_id})
|
| 382 |
+
|
| 383 |
+
@app.route('/analyze_media', methods=['POST'])
|
| 384 |
+
def analyze_media():
|
| 385 |
+
"""Runs the heavy CNN/SVM processing in the background."""
|
| 386 |
+
data = request.json
|
| 387 |
+
unique_id = data.get('video_id')
|
| 388 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(unique_id))
|
| 389 |
+
|
| 390 |
+
if not os.path.exists(file_path):
|
| 391 |
+
return jsonify({"error": "File not found"}), 404
|
| 392 |
+
|
| 393 |
+
try:
|
| 394 |
+
if models_loaded:
|
| 395 |
+
results = get_real_prediction(file_path, "N/A (Uploaded Media)")
|
| 396 |
+
else:
|
| 397 |
+
results = mock_predict("N/A (Uploaded Media)", file_path)
|
| 398 |
+
|
| 399 |
+
results['video_id'] = unique_id
|
| 400 |
+
results['is_video'] = True
|
| 401 |
+
return jsonify(results)
|
| 402 |
+
except Exception as e:
|
| 403 |
+
import traceback
|
| 404 |
+
traceback.print_exc()
|
| 405 |
+
return jsonify({"error": str(e)}), 500
|
| 406 |
+
|
| 407 |
+
@app.route('/predict_stream', methods=['POST'])
|
| 408 |
+
def predict_stream():
|
| 409 |
+
data = request.json
|
| 410 |
+
stream_url = data.get('url')
|
| 411 |
+
location = data.get('location', 'Unknown Location') # Default to browser location
|
| 412 |
+
|
| 413 |
+
if not stream_url:
|
| 414 |
+
return jsonify({"error": "No stream URL provided"}), 400
|
| 415 |
+
|
| 416 |
+
try:
|
| 417 |
+
# Sanitize Live IP Camera URLs (fix HTML entities)
|
| 418 |
+
if isinstance(stream_url, str):
|
| 419 |
+
stream_url = stream_url.replace('&', '&')
|
| 420 |
+
|
| 421 |
+
# --- OVERRIDE BROWSER LOCATION IF CAMERA IP CAN BE TRACED ---
|
| 422 |
+
cam_loc, cam_wx = get_camera_info_from_ip(stream_url)
|
| 423 |
+
if cam_loc:
|
| 424 |
+
location = cam_loc # Override browser location with actual Camera location!
|
| 425 |
+
|
| 426 |
+
cleanup_old_files()
|
| 427 |
+
|
| 428 |
+
cap = cv2.VideoCapture(stream_url)
|
| 429 |
+
if not cap.isOpened():
|
| 430 |
+
return jsonify({"error": "Failed to open stream. Check URL and connection."}), 400
|
| 431 |
+
|
| 432 |
+
frames = []
|
| 433 |
+
for _ in range(90):
|
| 434 |
+
ret, frame = cap.read()
|
| 435 |
+
if not ret: break
|
| 436 |
+
frames.append(frame)
|
| 437 |
+
cap.release()
|
| 438 |
+
|
| 439 |
+
if not frames:
|
| 440 |
+
return jsonify({"error": "Stream is empty or unreachable"}), 400
|
| 441 |
+
|
| 442 |
+
unique_id = f"stream_{uuid.uuid4().hex}.webm"
|
| 443 |
+
temp_path = os.path.join(app.config['UPLOAD_FOLDER'], unique_id)
|
| 444 |
+
height, width, _ = frames[0].shape
|
| 445 |
+
fourcc = cv2.VideoWriter_fourcc(*'VP80')
|
| 446 |
+
out = cv2.VideoWriter(temp_path, fourcc, 30.0, (width, height))
|
| 447 |
+
for f in frames:
|
| 448 |
+
out.write(f)
|
| 449 |
+
out.release()
|
| 450 |
+
|
| 451 |
+
if models_loaded:
|
| 452 |
+
results = get_real_prediction(temp_path, location)
|
| 453 |
+
else:
|
| 454 |
+
results = mock_predict(location, temp_path)
|
| 455 |
+
_, buffer = cv2.imencode('.jpg', frames[int(len(frames)/2)])
|
| 456 |
+
results['image'] = base64.b64encode(buffer).decode('utf-8')
|
| 457 |
+
|
| 458 |
+
# Pass the camera's location back to the frontend so the UI updates
|
| 459 |
+
if 'cam_loc' in locals() and cam_loc:
|
| 460 |
+
results['cam_location'] = cam_loc
|
| 461 |
+
results['cam_weather'] = cam_wx
|
| 462 |
+
|
| 463 |
+
results['video_id'] = unique_id
|
| 464 |
+
results['is_video'] = True
|
| 465 |
+
results['source_type'] = 'live'
|
| 466 |
+
return jsonify(results)
|
| 467 |
+
except Exception as e:
|
| 468 |
+
import traceback
|
| 469 |
+
traceback.print_exc()
|
| 470 |
+
return jsonify({"error": str(e)}), 500
|
| 471 |
+
|
| 472 |
+
@app.route('/predict_traffic_risk', methods=['POST'])
|
| 473 |
+
def predict_traffic_risk():
|
| 474 |
+
"""Handles the Tabular Predictor requests from the frontend."""
|
| 475 |
+
data = request.json
|
| 476 |
+
if not data:
|
| 477 |
+
return jsonify({"error": "No data provided"}), 400
|
| 478 |
+
|
| 479 |
+
try:
|
| 480 |
+
# If the model isn't found, return a mock simulation for demo purposes
|
| 481 |
+
if model_traffic is None:
|
| 482 |
+
import random
|
| 483 |
+
risk_prob = random.uniform(10, 85)
|
| 484 |
+
will_happen = risk_prob > 50
|
| 485 |
+
return jsonify({
|
| 486 |
+
"risk_probability_percentage": round(risk_prob, 1),
|
| 487 |
+
"will_accident_happen": will_happen,
|
| 488 |
+
"status": "High Risk Detected" if will_happen else "Low Risk Environment"
|
| 489 |
+
})
|
| 490 |
+
|
| 491 |
+
# Replace empty string payloads with None so Pandas treats them as True missing values (NaN)
|
| 492 |
+
cleaned_data = {k: (v if v != "" else None) for k, v in data.items()}
|
| 493 |
+
|
| 494 |
+
# Load data into pandas DataFrame
|
| 495 |
+
df = pd.DataFrame([cleaned_data])
|
| 496 |
+
|
| 497 |
+
# Ensure numeric columns are cast appropriately to prevent scikit-learn errors
|
| 498 |
+
numeric_cols = ['Traffic_Density', 'Speed_Limit', 'Number_of_Vehicles',
|
| 499 |
+
'Driver_Alcohol', 'Driver_Age', 'Driver_Experience']
|
| 500 |
+
for col in numeric_cols:
|
| 501 |
+
if col in df.columns:
|
| 502 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 503 |
+
|
| 504 |
+
# Feature Engineering (Robust against NaN values)
|
| 505 |
+
if 'Speed_Limit' in df.columns and 'Driver_Alcohol' in df.columns:
|
| 506 |
+
df['Speed_Alcohol_Risk'] = (df['Speed_Limit'] // 10) * (df['Driver_Alcohol'] + 0.1)
|
| 507 |
+
if 'Traffic_Density' in df.columns and 'Number_of_Vehicles' in df.columns:
|
| 508 |
+
df['Congestion_Risk'] = df['Traffic_Density'] * df['Number_of_Vehicles']
|
| 509 |
+
|
| 510 |
+
# Get prediction and probabilities
|
| 511 |
+
prediction = model_traffic.predict(df)[0]
|
| 512 |
+
probabilities = model_traffic.predict_proba(df)[0]
|
| 513 |
+
|
| 514 |
+
# Determine probability of accident (class 1)
|
| 515 |
+
accident_probability = probabilities[1] * 100
|
| 516 |
+
status = "High Risk Detected" if accident_probability >= 50 else "Low Risk Environment"
|
| 517 |
+
|
| 518 |
+
return jsonify({
|
| 519 |
+
"risk_probability_percentage": round(accident_probability, 1),
|
| 520 |
+
"will_accident_happen": bool(prediction),
|
| 521 |
+
"status": status
|
| 522 |
+
})
|
| 523 |
+
|
| 524 |
+
except Exception as e:
|
| 525 |
+
import traceback
|
| 526 |
+
traceback.print_exc()
|
| 527 |
+
return jsonify({"error": str(e)}), 500
|
| 528 |
+
|
| 529 |
+
@app.route('/video/<video_id>')
|
| 530 |
+
def get_video(video_id):
|
| 531 |
+
"""Serves the actual MP4 file for the HTML5 native video player"""
|
| 532 |
+
return send_from_directory(app.config['UPLOAD_FOLDER'], secure_filename(video_id))
|
| 533 |
+
|
| 534 |
+
@app.route('/stream_tracking/<video_id>')
|
| 535 |
+
def stream_tracking(video_id):
|
| 536 |
+
"""Real-time YOLO object tracking generator (yields MJPEG frames at normal video speed)."""
|
| 537 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(video_id))
|
| 538 |
+
|
| 539 |
+
def generate():
|
| 540 |
+
cap = cv2.VideoCapture(file_path)
|
| 541 |
+
fps = cap.get(cv2.CAP_PROP_FPS)
|
| 542 |
+
if fps <= 0: fps = 30
|
| 543 |
+
delay = 1.0 / fps # Calculate standard frame wait time
|
| 544 |
+
|
| 545 |
+
while cap.isOpened():
|
| 546 |
+
start_time = time.time()
|
| 547 |
+
ret, frame = cap.read()
|
| 548 |
+
if not ret:
|
| 549 |
+
cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # Loop video
|
| 550 |
+
continue
|
| 551 |
+
|
| 552 |
+
if models_loaded:
|
| 553 |
+
# Add bounding boxes over the frame (confidence kept low to ensure visibility)
|
| 554 |
+
res = model_yolo(frame, conf=0.25, verbose=False)[0]
|
| 555 |
+
frame = res.plot()
|
| 556 |
+
|
| 557 |
+
_, buffer = cv2.imencode('.jpg', frame)
|
| 558 |
+
yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + buffer.tobytes() + b'\r\n')
|
| 559 |
+
|
| 560 |
+
# Maintain native video speed without artificial slowdown
|
| 561 |
+
elapsed = time.time() - start_time
|
| 562 |
+
if elapsed < delay:
|
| 563 |
+
time.sleep(delay - elapsed)
|
| 564 |
+
cap.release()
|
| 565 |
+
|
| 566 |
+
return Response(generate(), mimetype='multipart/x-mixed-replace; boundary=frame')
|
| 567 |
+
|
| 568 |
+
if __name__ == '__main__':
|
| 569 |
+
app.run(debug=True, port=5000)
|
models/3dcnn_accident_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e5d1d4af597a3ad658d32cfabac0f9a1446f9af63d31ada359f74f786dd97c89
|
| 3 |
+
size 132753611
|
models/ensemble_svm_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d778d6f7d784e3ab0e2cd84094be07c680e764dcca06f1a23800733074b8a1ca
|
| 3 |
+
size 1987831
|
models/traffic_predictor.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f8aa5d74bd6e651abd8ad38610f1ade837db406e2c016d7a960adb77d1e9cd9c
|
| 3 |
+
size 29778
|
models/yolov8_accident_model.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4029939bb86ecee593cf0444fce12c7002859a7d9d90a67171fec6d87772531a
|
| 3 |
+
size 31690272
|
requirements.txt
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Flask
|
| 2 |
+
werkzeug
|
| 3 |
+
numpy
|
| 4 |
+
pandas
|
| 5 |
+
requests
|
| 6 |
+
Pillow
|
| 7 |
+
opencv-python-headless
|
| 8 |
+
torch
|
| 9 |
+
torchvision
|
| 10 |
+
scikit-learn
|
| 11 |
+
joblib
|
| 12 |
+
ultralytics
|
| 13 |
+
easyocr
|
| 14 |
+
tqdm
|
| 15 |
+
gunicorn
|
static/logo.PNG
ADDED
|
|
Git LFS Details
|
templates/index.html
ADDED
|
@@ -0,0 +1,1106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="light">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>CrashVision AI - Accident Detection</title>
|
| 7 |
+
<!-- Tailwind CSS -->
|
| 8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 9 |
+
<script>
|
| 10 |
+
tailwind.config = {
|
| 11 |
+
darkMode: 'class',
|
| 12 |
+
theme: {
|
| 13 |
+
extend: {
|
| 14 |
+
fontFamily: {
|
| 15 |
+
sans: ['-apple-system', 'BlinkMacSystemFont', 'San Francisco', 'Inter', 'sans-serif'],
|
| 16 |
+
},
|
| 17 |
+
animation: {
|
| 18 |
+
'blob': 'blob 10s infinite',
|
| 19 |
+
'pulse-glow': 'pulse-glow 3s infinite',
|
| 20 |
+
},
|
| 21 |
+
keyframes: {
|
| 22 |
+
blob: {
|
| 23 |
+
'0%': { transform: 'translate(0px, 0px) scale(1)' },
|
| 24 |
+
'33%': { transform: 'translate(40px, -60px) scale(1.2)' },
|
| 25 |
+
'66%': { transform: 'translate(-30px, 30px) scale(0.8)' },
|
| 26 |
+
'100%': { transform: 'translate(0px, 0px) scale(1)' },
|
| 27 |
+
},
|
| 28 |
+
'pulse-glow': {
|
| 29 |
+
'0%, 100%': { opacity: 0.6, transform: 'scale(1)' },
|
| 30 |
+
'50%': { opacity: 1, transform: 'scale(1.05)' },
|
| 31 |
+
}
|
| 32 |
+
}
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}
|
| 36 |
+
</script>
|
| 37 |
+
<!-- FontAwesome -->
|
| 38 |
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
| 39 |
+
<style>
|
| 40 |
+
body {
|
| 41 |
+
background-color: #F4F4F9;
|
| 42 |
+
-webkit-font-smoothing: antialiased;
|
| 43 |
+
transition: background-color 0.5s ease, background-image 0.5s ease;
|
| 44 |
+
}
|
| 45 |
+
.dark body {
|
| 46 |
+
background-color: #151518;
|
| 47 |
+
background-image: linear-gradient(135deg, #151518 0%, #2a1f3d 50%, #101012 100%);
|
| 48 |
+
background-attachment: fixed;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* New class to copy body background onto the predictive modal overlay */
|
| 52 |
+
.modal-bg {
|
| 53 |
+
background-color: #F4F4F9;
|
| 54 |
+
}
|
| 55 |
+
.dark .modal-bg {
|
| 56 |
+
background-color: #151518;
|
| 57 |
+
background-image: linear-gradient(135deg, #151518 0%, #2a1f3d 50%, #101012 100%);
|
| 58 |
+
background-attachment: fixed;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.ios-glass {
|
| 62 |
+
background: rgba(255, 255, 255, 0.45);
|
| 63 |
+
backdrop-filter: blur(40px) saturate(200%);
|
| 64 |
+
-webkit-backdrop-filter: blur(40px) saturate(200%);
|
| 65 |
+
border: 1.5px solid rgba(255, 255, 255, 0.9);
|
| 66 |
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5), 0 8px 32px rgba(0, 0, 0, 0.08);
|
| 67 |
+
transition: all 0.5s ease;
|
| 68 |
+
}
|
| 69 |
+
.dark .ios-glass {
|
| 70 |
+
background: rgba(20, 20, 25, 0.35);
|
| 71 |
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 72 |
+
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.05), 0 8px 32px rgba(0, 0, 0, 0.6);
|
| 73 |
+
backdrop-filter: blur(40px) saturate(200%);
|
| 74 |
+
-webkit-backdrop-filter: blur(40px) saturate(200%);
|
| 75 |
+
}
|
| 76 |
+
.ios-glass-inner {
|
| 77 |
+
background: rgba(255, 255, 255, 0.5);
|
| 78 |
+
border: 1px solid rgba(255, 255, 255, 0.8);
|
| 79 |
+
transition: all 0.5s ease;
|
| 80 |
+
}
|
| 81 |
+
.dark .ios-glass-inner {
|
| 82 |
+
background: rgba(255, 255, 255, 0.06);
|
| 83 |
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
| 84 |
+
}
|
| 85 |
+
.bg-mesh {
|
| 86 |
+
background-size: 40px 40px;
|
| 87 |
+
background-image:
|
| 88 |
+
linear-gradient(to right, rgba(100, 100, 150, 0.06) 1px, transparent 1px),
|
| 89 |
+
linear-gradient(to bottom, rgba(100, 100, 150, 0.06) 1px, transparent 1px);
|
| 90 |
+
}
|
| 91 |
+
.dark .bg-mesh {
|
| 92 |
+
background-image:
|
| 93 |
+
linear-gradient(to right, rgba(255, 255, 255, 0.03) 1px, transparent 1px),
|
| 94 |
+
linear-gradient(to bottom, rgba(255, 255, 255, 0.03) 1px, transparent 1px);
|
| 95 |
+
}
|
| 96 |
+
::-webkit-scrollbar { width: 6px; }
|
| 97 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 98 |
+
::-webkit-scrollbar-thumb { background: rgba(156, 163, 175, 0.5); border-radius: 10px; }
|
| 99 |
+
.dark ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); }
|
| 100 |
+
|
| 101 |
+
/* Custom input styling for the prediction form */
|
| 102 |
+
.predict-input {
|
| 103 |
+
width: 100%;
|
| 104 |
+
padding: 0.75rem 1rem;
|
| 105 |
+
border-radius: 0.75rem;
|
| 106 |
+
font-size: 0.875rem;
|
| 107 |
+
font-weight: 600;
|
| 108 |
+
outline: none;
|
| 109 |
+
transition: all 0.3s ease;
|
| 110 |
+
}
|
| 111 |
+
</style>
|
| 112 |
+
</head>
|
| 113 |
+
<body class="relative min-h-screen transition-colors duration-500 overflow-x-hidden">
|
| 114 |
+
|
| 115 |
+
<!-- Background Effects -->
|
| 116 |
+
<div class="fixed inset-0 z-0 bg-mesh pointer-events-none"></div>
|
| 117 |
+
<div class="fixed inset-0 z-0 overflow-hidden pointer-events-none">
|
| 118 |
+
<div class="absolute top-[5%] left-[10%] w-[35rem] h-[35rem] bg-indigo-400 dark:bg-purple-500/30 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-[120px] dark:blur-[140px] opacity-20 dark:opacity-30 animate-blob"></div>
|
| 119 |
+
<div class="absolute top-[20%] right-[10%] w-[30rem] h-[30rem] bg-fuchsia-300 dark:bg-indigo-500/20 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-[120px] opacity-20 dark:opacity-25 animate-blob" style="animation-delay: 2s"></div>
|
| 120 |
+
<div class="absolute bottom-[10%] left-[25%] w-[35rem] h-[35rem] bg-blue-300 dark:bg-fuchsia-500/20 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-[120px] dark:blur-[140px] opacity-20 dark:opacity-30 animate-blob" style="animation-delay: 4s"></div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Theme Toggle (Elevated z-index to stay above modals) -->
|
| 124 |
+
<div class="fixed top-6 right-6 sm:top-8 sm:right-8 z-[70]">
|
| 125 |
+
<button id="themeToggle" class="w-10 h-10 rounded-full flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-sm text-indigo-900 dark:text-purple-100 cursor-pointer bg-white/30 dark:bg-white/10 backdrop-blur-xl border border-white/40 dark:border-white/20">
|
| 126 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
| 127 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
| 128 |
+
</svg>
|
| 129 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 hidden dark:block drop-shadow-md" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
| 130 |
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-2.25l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
| 131 |
+
</svg>
|
| 132 |
+
</button>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<!-- Emergency Dispatch Banner -->
|
| 136 |
+
<div id="emergencyBanner" class="hidden fixed top-0 left-0 w-full bg-red-600 text-white py-3 px-4 z-50 flex items-center justify-center gap-4 animate-pulse shadow-lg shadow-red-500/50">
|
| 137 |
+
<i class="fas fa-exclamation-triangle text-xl"></i>
|
| 138 |
+
<span class="font-black uppercase tracking-widest text-sm sm:text-base">Major Collision Detected. Automated SOS Dispatched to Emergency Services.</span>
|
| 139 |
+
<i class="fas fa-exclamation-triangle text-xl"></i>
|
| 140 |
+
</div>
|
| 141 |
+
|
| 142 |
+
<header class="relative z-40 pt-8 sm:pt-16 pb-4 sm:pb-8 flex flex-col items-center justify-center mt-2 sm:mt-6">
|
| 143 |
+
<div class="mb-3 sm:mb-5 flex items-center justify-center group transition-all duration-500 hover:scale-110 drop-shadow-xl">
|
| 144 |
+
<svg class="w-12 h-12 sm:w-16 sm:h-16 z-10 text-indigo-700 dark:text-purple-400 transition-colors duration-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
| 145 |
+
<path d="M3 22L9 8H15L21 22" stroke-width="1.5" />
|
| 146 |
+
<path d="M12 22V17M12 13V11M12 9V8" stroke-width="1.5" />
|
| 147 |
+
<path d="M9.5 16L10.5 13H13.5L14.5 16V17.5H9.5V16Z" stroke-width="1.5" fill="currentColor" fill-opacity="0.1" />
|
| 148 |
+
<path d="M10.5 13L11 10.5H13L13.5 13" stroke-width="1.5" />
|
| 149 |
+
<path d="M8 9H6V11" stroke-width="2" />
|
| 150 |
+
<path d="M16 9H18V11" stroke-width="2" />
|
| 151 |
+
<path d="M8 19H6V17" stroke-width="2" />
|
| 152 |
+
<path d="M16 19H18V17" stroke-width="2" />
|
| 153 |
+
<circle cx="12" cy="14" r="1.5" fill="currentColor" stroke="none" class="animate-pulse" />
|
| 154 |
+
</svg>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div class="text-center">
|
| 158 |
+
<h1 class="text-3xl sm:text-5xl font-black tracking-tight leading-tight text-indigo-700 dark:text-purple-400 drop-shadow-sm transition-colors duration-500">
|
| 159 |
+
CrashVision AI
|
| 160 |
+
</h1>
|
| 161 |
+
<span class="text-[9px] sm:text-xs text-indigo-800/60 dark:text-purple-200 font-black tracking-[0.4em] uppercase mt-2 sm:mt-3 block opacity-90 transition-colors duration-500">
|
| 162 |
+
Road Safety Analytics
|
| 163 |
+
</span>
|
| 164 |
+
</div>
|
| 165 |
+
</header>
|
| 166 |
+
|
| 167 |
+
<main class="relative z-10 container mx-auto pb-12 px-4 sm:px-6 lg:px-8 max-w-6xl">
|
| 168 |
+
|
| 169 |
+
<div class="w-full transition-all duration-500">
|
| 170 |
+
|
| 171 |
+
<!-- SECTION 1: HOME AREA (Upload + Risk Island) -->
|
| 172 |
+
<div id="uploadSection" class="flex flex-col gap-6">
|
| 173 |
+
|
| 174 |
+
<!-- Incident Analysis Island (Video/IP) -->
|
| 175 |
+
<div class="ios-glass rounded-[32px] sm:rounded-[40px] p-6 sm:p-10 md:p-16 text-center shadow-lg">
|
| 176 |
+
<div class="max-w-3xl mx-auto">
|
| 177 |
+
<h2 class="text-2xl sm:text-3xl font-extrabold mb-2 sm:mb-4 tracking-tight text-indigo-950 dark:text-white">
|
| 178 |
+
Incident Analysis
|
| 179 |
+
</h2>
|
| 180 |
+
<p class="text-indigo-800/60 dark:text-purple-100 mb-6 sm:mb-10 text-xs sm:text-base leading-relaxed font-semibold">
|
| 181 |
+
Input surveillance streams or connect a live IP camera.
|
| 182 |
+
</p>
|
| 183 |
+
|
| 184 |
+
<div class="grid grid-cols-2 gap-3 sm:gap-6 justify-center">
|
| 185 |
+
<!-- IP Camera / Stream -->
|
| 186 |
+
<div class="relative group cursor-pointer h-full" onclick="openIpCamModal()">
|
| 187 |
+
<div class="block w-full h-full p-4 sm:p-10 border-2 border-dashed border-rose-400/30 dark:border-rose-400/40 rounded-[20px] sm:rounded-[32px] ios-glass-inner hover:bg-rose-50/70 dark:hover:bg-rose-900/20 transition-all group-hover:border-rose-500/50 dark:group-hover:border-rose-400/60 flex flex-col items-center justify-center">
|
| 188 |
+
<div class="w-12 h-12 sm:w-20 sm:h-20 bg-rose-50 dark:bg-rose-500/20 rounded-[14px] sm:rounded-3xl flex items-center justify-center mb-2 sm:mb-4 group-hover:scale-110 transition-transform duration-500 shadow-sm border border-rose-100 dark:border-rose-400/30">
|
| 189 |
+
<i class="fas fa-satellite-dish text-xl sm:text-3xl text-rose-600 dark:text-rose-300"></i>
|
| 190 |
+
</div>
|
| 191 |
+
<span class="block text-sm sm:text-xl font-black text-rose-900 dark:text-rose-100 mb-0 sm:mb-1 leading-tight">Live IP</span>
|
| 192 |
+
<span class="block text-[9px] sm:text-xs text-rose-600/60 dark:text-rose-300 font-bold uppercase tracking-widest hidden sm:block">Connect RTSP</span>
|
| 193 |
+
</div>
|
| 194 |
+
</div>
|
| 195 |
+
|
| 196 |
+
<!-- File Upload -->
|
| 197 |
+
<form id="uploadForm" class="relative group cursor-pointer h-full">
|
| 198 |
+
<input type="file" id="fileInput" name="file" accept="image/png, image/jpeg, image/jpg, video/mp4" class="hidden">
|
| 199 |
+
<label for="fileInput" class="block w-full h-full p-4 sm:p-10 border-2 border-dashed border-indigo-400/30 dark:border-purple-400/40 rounded-[20px] sm:rounded-[32px] ios-glass-inner hover:bg-white/70 dark:hover:bg-white/5 transition-all cursor-pointer group-hover:border-indigo-500/50 dark:group-hover:border-purple-300/60 flex flex-col items-center justify-center">
|
| 200 |
+
<div class="w-12 h-12 sm:w-20 sm:h-20 bg-indigo-50 dark:bg-white/10 rounded-[14px] sm:rounded-3xl flex items-center justify-center mb-2 sm:mb-4 group-hover:scale-110 transition-transform duration-500 shadow-sm border border-indigo-100 dark:border-white/20">
|
| 201 |
+
<i class="fas fa-arrow-up-from-bracket text-xl sm:text-3xl text-indigo-600 dark:text-purple-200"></i>
|
| 202 |
+
</div>
|
| 203 |
+
<span class="block text-sm sm:text-xl font-black text-indigo-900 dark:text-white mb-0 sm:mb-1 leading-tight">Select Media</span>
|
| 204 |
+
<span class="block text-[9px] sm:text-xs text-indigo-600/60 dark:text-purple-200 font-bold uppercase tracking-widest hidden sm:block">MP4 • JPEG • PNG</span>
|
| 205 |
+
</label>
|
| 206 |
+
</form>
|
| 207 |
+
</div>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
|
| 211 |
+
<!-- NEW: Traffic Risk Predictor Small Clickable Island -->
|
| 212 |
+
<div class="relative group cursor-pointer mt-2 mb-2" onclick="openPredictModal()">
|
| 213 |
+
<div class="ios-glass p-5 sm:p-6 rounded-[32px] hover:-translate-y-1 transition-transform duration-300 shadow-sm border border-cyan-400/30 hover:border-cyan-500/50 dark:border-cyan-500/20 dark:hover:border-cyan-400/40 flex items-center justify-between bg-gradient-to-r from-transparent to-cyan-50/30 dark:to-cyan-900/10">
|
| 214 |
+
<div class="flex items-center gap-5">
|
| 215 |
+
<div class="w-14 h-14 rounded-2xl bg-cyan-100 dark:bg-cyan-500/20 flex items-center justify-center shadow-sm group-hover:scale-110 transition-transform duration-300 border border-cyan-200/50 dark:border-cyan-400/30">
|
| 216 |
+
<i class="fas fa-chart-line text-cyan-600 dark:text-cyan-300 text-2xl"></i>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="text-left">
|
| 219 |
+
<h3 class="text-lg sm:text-xl font-black tracking-tight text-indigo-950 dark:text-white mb-0">Traffic Risk Predictor</h3>
|
| 220 |
+
<p class="text-[9px] sm:text-[10px] font-bold text-indigo-800/60 dark:text-purple-200 uppercase tracking-widest mt-1">Run Tabular AI Simulations</p>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
<div class="w-10 h-10 rounded-full bg-indigo-900/5 dark:bg-white/5 flex items-center justify-center group-hover:bg-cyan-100 dark:group-hover:bg-cyan-500/30 transition-colors">
|
| 224 |
+
<i class="fas fa-arrow-right text-indigo-400 dark:text-purple-300 group-hover:text-cyan-600 dark:group-hover:text-cyan-100 text-sm"></i>
|
| 225 |
+
</div>
|
| 226 |
+
</div>
|
| 227 |
+
</div>
|
| 228 |
+
|
| 229 |
+
<!-- 3 Columns Architecture Features -->
|
| 230 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 text-left">
|
| 231 |
+
<div class="ios-glass p-6 rounded-[32px] hover:-translate-y-1 transition-transform duration-300 shadow-sm cursor-default">
|
| 232 |
+
<div class="w-12 h-12 rounded-2xl bg-indigo-100 dark:bg-white/10 flex items-center justify-center mb-5 border border-indigo-200/50 dark:border-white/10 shadow-sm">
|
| 233 |
+
<i class="fas fa-eye text-indigo-600 dark:text-purple-300 text-xl"></i>
|
| 234 |
+
</div>
|
| 235 |
+
<h4 class="text-sm font-black uppercase tracking-wider text-indigo-950 dark:text-purple-50 mb-2">Spatial Engine</h4>
|
| 236 |
+
<p class="text-xs text-indigo-800/70 dark:text-purple-100 leading-relaxed font-bold">YOLOv8 Medium with XAI bounding boxes.</p>
|
| 237 |
+
</div>
|
| 238 |
+
<div class="ios-glass p-6 rounded-[32px] hover:-translate-y-1 transition-transform duration-300 shadow-sm cursor-default">
|
| 239 |
+
<div class="w-12 h-12 rounded-2xl bg-fuchsia-100 dark:bg-white/10 flex items-center justify-center mb-5 border border-fuchsia-200/50 dark:border-white/10 shadow-sm">
|
| 240 |
+
<i class="fas fa-layer-group text-fuchsia-600 dark:text-fuchsia-300 text-xl"></i>
|
| 241 |
+
</div>
|
| 242 |
+
<h4 class="text-sm font-black uppercase tracking-wider text-indigo-950 dark:text-purple-50 mb-2">Temporal Engine</h4>
|
| 243 |
+
<p class="text-xs text-indigo-800/70 dark:text-purple-100 leading-relaxed font-bold">3D-CNN models physical motion.</p>
|
| 244 |
+
</div>
|
| 245 |
+
<div class="ios-glass p-6 rounded-[32px] hover:-translate-y-1 transition-transform duration-300 shadow-sm cursor-default">
|
| 246 |
+
<div class="w-12 h-12 rounded-2xl bg-rose-100 dark:bg-white/10 flex items-center justify-center mb-5 border border-rose-200/50 dark:border-white/10 shadow-sm">
|
| 247 |
+
<i class="fas fa-bolt text-rose-600 dark:text-rose-300 text-xl"></i>
|
| 248 |
+
</div>
|
| 249 |
+
<h4 class="text-sm font-black uppercase tracking-wider text-indigo-950 dark:text-purple-50 mb-2">Automated SOS</h4>
|
| 250 |
+
<p class="text-xs text-indigo-800/70 dark:text-purple-100 leading-relaxed font-bold">Dispatch triggered by SVM Ensemble.</p>
|
| 251 |
+
</div>
|
| 252 |
+
</div>
|
| 253 |
+
|
| 254 |
+
</div>
|
| 255 |
+
|
| 256 |
+
<!-- SECTION 2: LOADING -->
|
| 257 |
+
<div id="loading" class="hidden ios-glass rounded-[40px] p-24 text-center shadow-xl">
|
| 258 |
+
<div class="relative w-28 h-28 mx-auto mb-10">
|
| 259 |
+
<div class="absolute inset-0 border-[4px] border-indigo-200/50 dark:border-white/20 rounded-full"></div>
|
| 260 |
+
<div class="absolute inset-0 border-[4px] border-t-transparent border-l-transparent border-indigo-600 dark:border-purple-300 rounded-full animate-spin dark:shadow-[0_0_15px_var(--purple-300)]"></div>
|
| 261 |
+
<div class="absolute inset-4 bg-white/50 dark:bg-white/10 rounded-full backdrop-blur-xl flex items-center justify-center border border-white/40 dark:border-white/20">
|
| 262 |
+
<i id="loadingIcon" class="fas fa-brain text-2xl text-indigo-600 dark:text-fuchsia-300 animate-pulse"></i>
|
| 263 |
+
</div>
|
| 264 |
+
</div>
|
| 265 |
+
<h3 id="loadingText" class="text-2xl font-black mb-3 tracking-tight text-indigo-950 dark:text-white">Synchronizing AI Layers</h3>
|
| 266 |
+
<p id="loadingSub" class="text-indigo-800/60 dark:text-purple-200 animate-pulse text-sm font-black uppercase tracking-widest">Weighted Ensemble Inference...</p>
|
| 267 |
+
</div>
|
| 268 |
+
|
| 269 |
+
<!-- SECTION 3: RESULTS DASHBOARD (Visual Detection) -->
|
| 270 |
+
<div id="results" class="hidden ios-glass rounded-[40px] overflow-hidden shadow-2xl">
|
| 271 |
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-0">
|
| 272 |
+
|
| 273 |
+
<!-- Left: Media View -->
|
| 274 |
+
<div class="lg:col-span-7 p-8 md:p-12 border-b lg:border-b-0 lg:border-r border-indigo-200/50 dark:border-white/10">
|
| 275 |
+
<div class="flex justify-between items-center mb-6">
|
| 276 |
+
<h3 class="text-xs font-black uppercase tracking-[0.3em] text-indigo-800/50 dark:text-purple-200">XAI Analytics Feed</h3>
|
| 277 |
+
<span class="px-4 py-1.5 text-[10px] font-black uppercase tracking-widest bg-white/60 dark:bg-white/10 rounded-full text-indigo-900 dark:text-white border border-indigo-200/50 dark:border-white/20 shadow-sm">Channel: CV-PRIME</span>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
<!-- Interactive Media Player Area -->
|
| 281 |
+
<div class="rounded-[32px] overflow-hidden shadow-2xl border border-white/50 dark:border-white/20 relative bg-black aspect-video flex items-center justify-center group">
|
| 282 |
+
<!-- Display Image -->
|
| 283 |
+
<img id="previewImg" src="" class="w-full h-full object-contain z-10 hidden" alt="Detection View">
|
| 284 |
+
<!-- Standard Video Player -->
|
| 285 |
+
<video id="previewVideo" controls class="w-full h-full object-contain hidden z-10 bg-black"></video>
|
| 286 |
+
|
| 287 |
+
<!-- Live Tag -->
|
| 288 |
+
<div class="absolute top-6 left-6 z-20">
|
| 289 |
+
<span id="liveBadge" class="bg-black/60 backdrop-blur-xl text-white text-[10px] px-3 py-1.5 rounded-xl font-black uppercase tracking-widest border border-white/20 flex items-center gap-2">
|
| 290 |
+
<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse shadow-[0_0_8px_red]"></span> XAI SNAPSHOT
|
| 291 |
+
</span>
|
| 292 |
+
</div>
|
| 293 |
+
</div>
|
| 294 |
+
|
| 295 |
+
<!-- Media Controls (Moved completely underneath the video) -->
|
| 296 |
+
<div id="mediaControls" class="flex justify-center gap-4 mt-6 hidden">
|
| 297 |
+
<button onclick="showSnapshot()" class="px-5 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-indigo-900 dark:text-white bg-white/50 dark:bg-white/10 border border-indigo-200/50 dark:border-white/20 hover:scale-105 transition-transform shadow-sm flex items-center gap-2">
|
| 298 |
+
<i class="fas fa-camera"></i> Snapshot
|
| 299 |
+
</button>
|
| 300 |
+
<button onclick="playTracking()" class="px-5 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-white hover:bg-rose-500/80 transition-all bg-rose-600 shadow-lg shadow-rose-500/50 flex items-center gap-2">
|
| 301 |
+
<i class="fas fa-brain"></i> AI Tracking
|
| 302 |
+
</button>
|
| 303 |
+
<button onclick="playOriginal()" class="px-5 py-3 rounded-xl text-[10px] font-black uppercase tracking-widest text-indigo-900 dark:text-white bg-white/50 dark:bg-white/10 border border-indigo-200/50 dark:border-white/20 hover:scale-105 transition-transform shadow-sm flex items-center gap-2">
|
| 304 |
+
<i class="fas fa-play"></i> Original
|
| 305 |
+
</button>
|
| 306 |
+
</div>
|
| 307 |
+
|
| 308 |
+
<div class="grid grid-cols-3 gap-5 mt-10">
|
| 309 |
+
<div class="ios-glass-inner p-5 rounded-3xl flex flex-col items-center text-center gap-1 hover:bg-white/60 dark:hover:bg-white/10 transition-colors">
|
| 310 |
+
<i class="fas fa-location-dot text-rose-500 text-xl mb-2"></i>
|
| 311 |
+
<p class="text-[9px] uppercase tracking-widest text-indigo-800/50 dark:text-purple-200 font-black">Location</p>
|
| 312 |
+
<p id="realLocation" class="text-xs font-black text-indigo-950 dark:text-white truncate w-full tracking-tight text-center px-1">Detecting...</p>
|
| 313 |
+
</div>
|
| 314 |
+
<div class="ios-glass-inner p-5 rounded-3xl flex flex-col items-center text-center gap-1 hover:bg-white/60 dark:hover:bg-white/10 transition-colors">
|
| 315 |
+
<i class="fas fa-cloud-bolt text-cyan-500 text-xl mb-2"></i>
|
| 316 |
+
<p class="text-[9px] uppercase tracking-widest text-indigo-800/50 dark:text-purple-200 font-black">Conditions</p>
|
| 317 |
+
<p id="realWeather" class="text-xs font-black text-indigo-950 dark:text-white truncate w-full tracking-tight text-center">Detecting...</p>
|
| 318 |
+
</div>
|
| 319 |
+
<div class="ios-glass-inner p-5 rounded-3xl flex flex-col items-center text-center gap-1 hover:bg-white/60 dark:hover:bg-white/10 transition-colors">
|
| 320 |
+
<i class="fas fa-stopwatch text-indigo-500 dark:text-purple-300 text-xl mb-2"></i>
|
| 321 |
+
<p class="text-[9px] uppercase tracking-widest text-indigo-800/50 dark:text-purple-200 font-black">Timeline</p>
|
| 322 |
+
<p id="realTime" class="text-xs font-black text-indigo-950 dark:text-white truncate w-full tracking-tight text-center">--</p>
|
| 323 |
+
</div>
|
| 324 |
+
</div>
|
| 325 |
+
</div>
|
| 326 |
+
|
| 327 |
+
<!-- Right: AI Diagnostics -->
|
| 328 |
+
<div class="lg:col-span-5 p-8 md:p-12 bg-white/20 dark:bg-black/20 flex flex-col justify-between relative">
|
| 329 |
+
<div>
|
| 330 |
+
<h3 class="text-xs font-black uppercase tracking-[0.3em] text-indigo-800/50 dark:text-purple-200 mb-8">AI Diagnostics</h3>
|
| 331 |
+
|
| 332 |
+
<div id="statusCard" class="ios-glass-inner p-8 rounded-[32px] mb-8 shadow-sm dark:shadow-lg relative overflow-hidden">
|
| 333 |
+
<div id="statusLine" class="absolute top-0 left-0 w-2 h-full bg-indigo-500 dark:bg-purple-400"></div>
|
| 334 |
+
|
| 335 |
+
<div class="flex items-start justify-between pl-2">
|
| 336 |
+
<div>
|
| 337 |
+
<div id="statusBadge" class="inline-flex items-center gap-2 px-3 py-1.5 rounded-xl text-[10px] font-black mb-4 uppercase tracking-[0.2em] border border-indigo-200/50 dark:border-white/20 shadow-sm bg-white/60 dark:bg-white/10 text-indigo-900 dark:text-white">
|
| 338 |
+
<i id="statusIcon" class="fas"></i>
|
| 339 |
+
<span id="labelText">Scanning...</span>
|
| 340 |
+
</div>
|
| 341 |
+
<h4 class="text-5xl font-black tracking-tighter text-indigo-950 dark:text-white mb-1"><span id="confidenceValue">0</span>%</h4>
|
| 342 |
+
<p class="text-indigo-800/60 dark:text-purple-200 text-[10px] font-black uppercase tracking-widest">Ensemble Confidence</p>
|
| 343 |
+
</div>
|
| 344 |
+
<div id="severityContainer" class="text-center bg-white dark:bg-white/10 px-4 py-3 rounded-2xl shadow-sm border border-indigo-100 dark:border-white/20 hidden">
|
| 345 |
+
<span class="block text-[9px] uppercase tracking-widest text-indigo-500 dark:text-fuchsia-300 font-black mb-1">Severity</span>
|
| 346 |
+
<span id="severityLevel" class="font-black text-sm uppercase">--</span>
|
| 347 |
+
</div>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
+
<div class="space-y-7 mb-10">
|
| 352 |
+
<div>
|
| 353 |
+
<div class="flex justify-between items-end mb-3">
|
| 354 |
+
<span class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-50 flex items-center gap-3">
|
| 355 |
+
<div class="w-8 h-8 rounded-xl bg-white/60 dark:bg-white/10 flex items-center justify-center border border-indigo-200/50 dark:border-white/20 shadow-sm">
|
| 356 |
+
<i class="fas fa-car-side text-indigo-600 dark:text-fuchsia-300 text-xs"></i>
|
| 357 |
+
</div>
|
| 358 |
+
ALPR (License Plates)
|
| 359 |
+
</span>
|
| 360 |
+
</div>
|
| 361 |
+
<div id="alprContainer" class="w-full bg-indigo-50 dark:bg-white/5 rounded-2xl p-4 border border-indigo-100 dark:border-white/10 flex flex-wrap gap-2 shadow-inner">
|
| 362 |
+
<span class="text-xs font-black text-indigo-900 dark:text-white">Scanning...</span>
|
| 363 |
+
</div>
|
| 364 |
+
</div>
|
| 365 |
+
|
| 366 |
+
<div>
|
| 367 |
+
<div class="flex justify-between items-end mb-3">
|
| 368 |
+
<span class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-50 flex items-center gap-3">
|
| 369 |
+
<div class="w-8 h-8 rounded-xl bg-white/60 dark:bg-white/10 flex items-center justify-center border border-indigo-200/50 dark:border-white/20 shadow-sm">
|
| 370 |
+
<i class="fas fa-layer-group text-indigo-600 dark:text-fuchsia-300 text-xs"></i>
|
| 371 |
+
</div>
|
| 372 |
+
Temporal (3D-CNN)
|
| 373 |
+
</span>
|
| 374 |
+
<span id="slowfastScore" class="font-black text-sm text-indigo-900 dark:text-white">--%</span>
|
| 375 |
+
</div>
|
| 376 |
+
<div class="w-full bg-indigo-200/50 dark:bg-white/20 rounded-full h-3 overflow-hidden border border-white/40 dark:border-white/10">
|
| 377 |
+
<div id="slowfastBar" class="bg-gradient-to-r from-indigo-500 to-fuchsia-500 dark:from-purple-500 dark:to-fuchsia-400 h-full rounded-full transition-all duration-1000 ease-out shadow-sm" style="width: 0%"></div>
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
<div>
|
| 381 |
+
<div class="flex justify-between items-end mb-3">
|
| 382 |
+
<span class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-50 flex items-center gap-3">
|
| 383 |
+
<div class="w-8 h-8 rounded-xl bg-white/60 dark:bg-white/10 flex items-center justify-center border border-indigo-200/50 dark:border-white/20 shadow-sm">
|
| 384 |
+
<i class="fas fa-crosshairs text-amber-600 dark:text-amber-400 text-xs"></i>
|
| 385 |
+
</div>
|
| 386 |
+
Spatial (YOLOv8 Max)
|
| 387 |
+
</span>
|
| 388 |
+
<span id="yoloScore" class="font-black text-sm text-indigo-900 dark:text-white">--%</span>
|
| 389 |
+
</div>
|
| 390 |
+
<div class="w-full bg-indigo-200/50 dark:bg-white/20 rounded-full h-3 overflow-hidden border border-white/40 dark:border-white/10">
|
| 391 |
+
<div id="yoloBar" class="bg-gradient-to-r from-amber-400 to-orange-500 dark:from-fuchsia-400 dark:to-orange-400 h-full rounded-full transition-all duration-1000 ease-out shadow-sm" style="width: 0%"></div>
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
</div>
|
| 395 |
+
</div>
|
| 396 |
+
|
| 397 |
+
<div class="mt-8 flex gap-4">
|
| 398 |
+
<button onclick="resetApp()" class="flex-1 py-4 px-6 bg-indigo-950 dark:bg-purple-600 text-white rounded-2xl font-black uppercase tracking-widest transition-all active:scale-95 text-xs flex justify-center items-center gap-3 shadow-xl dark:shadow-purple-500/40 hover:opacity-90 dark:hover:bg-purple-500">
|
| 399 |
+
<i class="fas fa-plus text-white"></i> New Scan
|
| 400 |
+
</button>
|
| 401 |
+
</div>
|
| 402 |
+
</div>
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
</div>
|
| 406 |
+
</main>
|
| 407 |
+
|
| 408 |
+
<!-- IP Camera Modal -->
|
| 409 |
+
<div id="ipCamModal" class="hidden fixed inset-0 z-[60] flex items-center justify-center bg-indigo-950/60 dark:bg-black/80 backdrop-blur-md p-4">
|
| 410 |
+
<div class="ios-glass p-8 rounded-[32px] max-w-md w-full shadow-2xl relative">
|
| 411 |
+
<h3 class="text-2xl font-black text-indigo-950 dark:text-white mb-2">Connect IP Camera</h3>
|
| 412 |
+
<p class="text-sm font-bold text-indigo-800/60 dark:text-purple-200 mb-6">Enter the RTSP or HTTP stream URL for your surveillance camera.</p>
|
| 413 |
+
|
| 414 |
+
<input type="text" id="ipCamUrl" placeholder="e.g., http://192.168.1.5:8080/video" class="w-full px-5 py-4 rounded-xl border border-indigo-200 dark:border-white/20 bg-white/50 dark:bg-white/5 text-indigo-900 dark:text-white font-semibold outline-none focus:border-indigo-500 dark:focus:border-purple-400 mb-6 placeholder-indigo-300 dark:placeholder-white/30">
|
| 415 |
+
|
| 416 |
+
<div class="flex gap-4">
|
| 417 |
+
<button onclick="closeIpCamModal()" class="flex-1 py-3 rounded-xl font-black uppercase tracking-widest text-xs bg-white dark:bg-white/10 text-indigo-900 dark:text-white border border-indigo-200 dark:border-white/20 shadow-sm hover:bg-gray-50 dark:hover:bg-white/20 transition-all">Cancel</button>
|
| 418 |
+
<button onclick="connectIpCam()" class="flex-1 py-3 rounded-xl font-black uppercase tracking-widest text-xs bg-rose-600 text-white shadow-lg shadow-rose-500/30 hover:bg-rose-500 transition-all">Connect</button>
|
| 419 |
+
</div>
|
| 420 |
+
</div>
|
| 421 |
+
</div>
|
| 422 |
+
|
| 423 |
+
<!-- Predictive Analysis Form Modal (Overlay Screen) -->
|
| 424 |
+
<div id="predictiveModal" class="hidden fixed inset-0 z-[60] modal-bg overflow-y-auto transition-colors duration-500">
|
| 425 |
+
|
| 426 |
+
<!-- Background Mesh & Animated Blobs to match Theme -->
|
| 427 |
+
<div class="fixed inset-0 z-0 bg-mesh pointer-events-none"></div>
|
| 428 |
+
<div class="fixed inset-0 z-0 overflow-hidden pointer-events-none">
|
| 429 |
+
<div class="absolute top-[10%] left-[20%] w-[35rem] h-[35rem] bg-cyan-400 dark:bg-cyan-500/20 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-[120px] dark:blur-[140px] opacity-20 dark:opacity-30 animate-blob"></div>
|
| 430 |
+
<div class="absolute bottom-[20%] right-[10%] w-[30rem] h-[30rem] bg-indigo-300 dark:bg-indigo-500/20 rounded-full mix-blend-multiply dark:mix-blend-screen filter blur-[120px] opacity-20 dark:opacity-25 animate-blob" style="animation-delay: 2s"></div>
|
| 431 |
+
</div>
|
| 432 |
+
|
| 433 |
+
<!-- Back Button (Top Left) -->
|
| 434 |
+
<button onclick="closePredictModal()" class="fixed top-6 left-6 sm:top-8 sm:left-8 z-[70] w-10 h-10 rounded-full flex items-center justify-center hover:scale-105 active:scale-95 transition-all shadow-sm text-indigo-900 dark:text-purple-100 cursor-pointer bg-white/30 dark:bg-white/10 backdrop-blur-xl border border-white/40 dark:border-white/20">
|
| 435 |
+
<i class="fas fa-arrow-left text-sm"></i>
|
| 436 |
+
</button>
|
| 437 |
+
|
| 438 |
+
<!-- Main Content Wrapper -->
|
| 439 |
+
<div class="relative z-10 min-h-screen flex flex-col">
|
| 440 |
+
|
| 441 |
+
<!-- Header (Text Outside the Island) -->
|
| 442 |
+
<div class="pt-20 sm:pt-24 pb-8 px-4 flex flex-col items-center justify-center text-center">
|
| 443 |
+
<div class="mb-4 sm:mb-6 flex items-center justify-center group transition-all duration-500 hover:scale-110 drop-shadow-xl">
|
| 444 |
+
<div class="w-14 h-14 sm:w-16 sm:h-16 rounded-2xl bg-cyan-100 dark:bg-cyan-500/20 flex items-center justify-center border border-cyan-200/50 dark:border-cyan-400/30 shadow-sm relative overflow-hidden">
|
| 445 |
+
<i class="fas fa-chart-line text-cyan-600 dark:text-cyan-300 text-2xl sm:text-3xl z-10"></i>
|
| 446 |
+
</div>
|
| 447 |
+
</div>
|
| 448 |
+
<h2 class="text-3xl sm:text-4xl font-black tracking-tight text-indigo-950 dark:text-white mb-3">Traffic Risk Predictor</h2>
|
| 449 |
+
<p class="text-indigo-800/60 dark:text-purple-200 text-xs sm:text-sm font-semibold max-w-xl mx-auto px-4">
|
| 450 |
+
Predict the probability of an accident using our trained Tabular AI model. Adjust the parameters below to run a simulation.
|
| 451 |
+
</p>
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
<!-- Island Container (Smaller: max-w-4xl) -->
|
| 455 |
+
<div class="container mx-auto px-4 pb-16 max-w-4xl flex-grow">
|
| 456 |
+
<div class="ios-glass p-6 sm:p-10 rounded-[32px] sm:rounded-[40px] shadow-2xl w-full">
|
| 457 |
+
|
| 458 |
+
<!-- Form View -->
|
| 459 |
+
<div id="predictiveFormView">
|
| 460 |
+
<form id="tabularForm" onsubmit="submitPredictiveData(event)">
|
| 461 |
+
<!-- Restructured Grid: 2 Columns for 4 sections -->
|
| 462 |
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
| 463 |
+
|
| 464 |
+
<!-- Group 1: Environment Island -->
|
| 465 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 466 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 467 |
+
<div class="w-8 h-8 rounded-xl bg-cyan-100 dark:bg-cyan-500/20 flex items-center justify-center border border-cyan-200/50 dark:border-cyan-400/30 group-hover:scale-110 transition-transform">
|
| 468 |
+
<i class="fas fa-cloud-sun text-cyan-600 dark:text-cyan-300 text-xs"></i>
|
| 469 |
+
</div>
|
| 470 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Conditions</h4>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="space-y-4">
|
| 473 |
+
<div>
|
| 474 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Weather</label>
|
| 475 |
+
<select id="tab_weather" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 shadow-sm cursor-pointer">
|
| 476 |
+
<option value="">Unknown / Leave Blank</option>
|
| 477 |
+
<option value="Clear">Clear</option>
|
| 478 |
+
<option value="Rainy">Rainy</option>
|
| 479 |
+
<option value="Foggy">Foggy</option>
|
| 480 |
+
<option value="Snowy">Snowy</option>
|
| 481 |
+
</select>
|
| 482 |
+
</div>
|
| 483 |
+
<div>
|
| 484 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Light Cond.</label>
|
| 485 |
+
<select id="tab_light" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 shadow-sm cursor-pointer">
|
| 486 |
+
<option value="">Unknown / Leave Blank</option>
|
| 487 |
+
<option value="Daylight">Daylight</option>
|
| 488 |
+
<option value="Artificial Light">Artificial Light</option>
|
| 489 |
+
<option value="No Light">No Light</option>
|
| 490 |
+
</select>
|
| 491 |
+
</div>
|
| 492 |
+
<div>
|
| 493 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Time of Day</label>
|
| 494 |
+
<select id="tab_time" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-cyan-500 focus:ring-2 focus:ring-cyan-500/20 shadow-sm cursor-pointer">
|
| 495 |
+
<option value="">Unknown / Leave Blank</option>
|
| 496 |
+
<option value="Morning">Morning</option>
|
| 497 |
+
<option value="Afternoon">Afternoon</option>
|
| 498 |
+
<option value="Evening">Evening</option>
|
| 499 |
+
<option value="Night">Night</option>
|
| 500 |
+
</select>
|
| 501 |
+
</div>
|
| 502 |
+
</div>
|
| 503 |
+
</div>
|
| 504 |
+
|
| 505 |
+
<!-- Group 2: Road Specs Island -->
|
| 506 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 507 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 508 |
+
<div class="w-8 h-8 rounded-xl bg-rose-100 dark:bg-rose-500/20 flex items-center justify-center border border-rose-200/50 dark:border-rose-400/30 group-hover:scale-110 transition-transform">
|
| 509 |
+
<i class="fas fa-road text-rose-600 dark:text-rose-300 text-xs"></i>
|
| 510 |
+
</div>
|
| 511 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Road Specs</h4>
|
| 512 |
+
</div>
|
| 513 |
+
<div class="space-y-4">
|
| 514 |
+
<div>
|
| 515 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Road Type</label>
|
| 516 |
+
<select id="tab_road_type" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm cursor-pointer">
|
| 517 |
+
<option value="">Unknown / Leave Blank</option>
|
| 518 |
+
<option value="City Road">City Road</option>
|
| 519 |
+
<option value="Highway">Highway</option>
|
| 520 |
+
<option value="Rural Road">Rural Road</option>
|
| 521 |
+
</select>
|
| 522 |
+
</div>
|
| 523 |
+
<div>
|
| 524 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Surface</label>
|
| 525 |
+
<select id="tab_road_cond" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm cursor-pointer">
|
| 526 |
+
<option value="">Unknown / Leave Blank</option>
|
| 527 |
+
<option value="Dry">Dry</option>
|
| 528 |
+
<option value="Wet">Wet</option>
|
| 529 |
+
<option value="Icy">Icy</option>
|
| 530 |
+
<option value="Under Construction">Under Construction</option>
|
| 531 |
+
</select>
|
| 532 |
+
</div>
|
| 533 |
+
<div>
|
| 534 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Speed Limit</label>
|
| 535 |
+
<input type="number" id="tab_speed" placeholder="e.g. 60" min="10" max="250" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm">
|
| 536 |
+
</div>
|
| 537 |
+
</div>
|
| 538 |
+
</div>
|
| 539 |
+
|
| 540 |
+
<!-- Group 3: Traffic Details Island -->
|
| 541 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 542 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 543 |
+
<div class="w-8 h-8 rounded-xl bg-amber-100 dark:bg-amber-500/20 flex items-center justify-center border border-amber-200/50 dark:border-amber-400/30 group-hover:scale-110 transition-transform">
|
| 544 |
+
<i class="fas fa-car-side text-amber-600 dark:text-amber-300 text-xs"></i>
|
| 545 |
+
</div>
|
| 546 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Traffic Data</h4>
|
| 547 |
+
</div>
|
| 548 |
+
<div class="space-y-4">
|
| 549 |
+
<div>
|
| 550 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Density (0-5)</label>
|
| 551 |
+
<input type="number" id="tab_density" placeholder="e.g. 2" min="0" max="5" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm">
|
| 552 |
+
</div>
|
| 553 |
+
<div>
|
| 554 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Vehicles Inv.</label>
|
| 555 |
+
<input type="number" id="tab_vehicles" placeholder="e.g. 2" min="1" max="20" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm">
|
| 556 |
+
</div>
|
| 557 |
+
<div>
|
| 558 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Vehicle Type</label>
|
| 559 |
+
<select id="tab_vehicle" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm cursor-pointer">
|
| 560 |
+
<option value="">Unknown / Leave Blank</option>
|
| 561 |
+
<option value="Car">Car</option>
|
| 562 |
+
<option value="Truck">Truck</option>
|
| 563 |
+
<option value="Bus">Bus</option>
|
| 564 |
+
<option value="Motorcycle">Motorcycle</option>
|
| 565 |
+
</select>
|
| 566 |
+
</div>
|
| 567 |
+
</div>
|
| 568 |
+
</div>
|
| 569 |
+
|
| 570 |
+
<!-- Group 4: Driver Metrics Island -->
|
| 571 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 572 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 573 |
+
<div class="w-8 h-8 rounded-xl bg-fuchsia-100 dark:bg-fuchsia-500/20 flex items-center justify-center border border-fuchsia-200/50 dark:border-fuchsia-400/30 group-hover:scale-110 transition-transform">
|
| 574 |
+
<i class="fas fa-user text-fuchsia-600 dark:text-fuchsia-300 text-xs"></i>
|
| 575 |
+
</div>
|
| 576 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Driver Profile</h4>
|
| 577 |
+
</div>
|
| 578 |
+
<div class="space-y-4">
|
| 579 |
+
<div>
|
| 580 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Driver Age</label>
|
| 581 |
+
<input type="number" id="tab_age" placeholder="e.g. 35" min="16" max="99" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 582 |
+
</div>
|
| 583 |
+
<div>
|
| 584 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Experience (Yrs)</label>
|
| 585 |
+
<input type="number" id="tab_exp" placeholder="e.g. 10" min="0" max="80" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 586 |
+
</div>
|
| 587 |
+
<div>
|
| 588 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Alcohol Lvl (0-1)</label>
|
| 589 |
+
<input type="number" id="tab_alcohol" placeholder="e.g. 0.0" step="0.1" min="0" max="1" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 590 |
+
</div>
|
| 591 |
+
</div>
|
| 592 |
+
</div>
|
| 593 |
+
|
| 594 |
+
<!-- Group 2: Road Specs Island -->
|
| 595 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 596 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 597 |
+
<div class="w-8 h-8 rounded-xl bg-rose-100 dark:bg-rose-500/20 flex items-center justify-center border border-rose-200/50 dark:border-rose-400/30 group-hover:scale-110 transition-transform">
|
| 598 |
+
<i class="fas fa-road text-rose-600 dark:text-rose-300 text-xs"></i>
|
| 599 |
+
</div>
|
| 600 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Road Specs</h4>
|
| 601 |
+
</div>
|
| 602 |
+
<div class="space-y-4">
|
| 603 |
+
<div>
|
| 604 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Road Type</label>
|
| 605 |
+
<select id="tab_road_type" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm cursor-pointer">
|
| 606 |
+
<option value="City Road">City Road</option>
|
| 607 |
+
<option value="Highway">Highway</option>
|
| 608 |
+
<option value="Rural Road">Rural Road</option>
|
| 609 |
+
</select>
|
| 610 |
+
</div>
|
| 611 |
+
<div>
|
| 612 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Surface</label>
|
| 613 |
+
<select id="tab_road_cond" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm cursor-pointer">
|
| 614 |
+
<option value="Dry">Dry</option>
|
| 615 |
+
<option value="Wet">Wet</option>
|
| 616 |
+
<option value="Icy">Icy</option>
|
| 617 |
+
<option value="Under Construction">Under Construction</option>
|
| 618 |
+
</select>
|
| 619 |
+
</div>
|
| 620 |
+
<div>
|
| 621 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Speed Limit</label>
|
| 622 |
+
<input type="number" id="tab_speed" value="60" min="10" max="250" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-rose-500 focus:ring-2 focus:ring-rose-500/20 shadow-sm">
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
</div>
|
| 626 |
+
|
| 627 |
+
<!-- Group 3: Traffic Details Island -->
|
| 628 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 629 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 630 |
+
<div class="w-8 h-8 rounded-xl bg-amber-100 dark:bg-amber-500/20 flex items-center justify-center border border-amber-200/50 dark:border-amber-400/30 group-hover:scale-110 transition-transform">
|
| 631 |
+
<i class="fas fa-car-side text-amber-600 dark:text-amber-300 text-xs"></i>
|
| 632 |
+
</div>
|
| 633 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Traffic Data</h4>
|
| 634 |
+
</div>
|
| 635 |
+
<div class="space-y-4">
|
| 636 |
+
<div>
|
| 637 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Density (0-5)</label>
|
| 638 |
+
<input type="number" id="tab_density" value="2" min="0" max="5" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm">
|
| 639 |
+
</div>
|
| 640 |
+
<div>
|
| 641 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Vehicles Inv.</label>
|
| 642 |
+
<input type="number" id="tab_vehicles" value="2" min="1" max="20" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm">
|
| 643 |
+
</div>
|
| 644 |
+
<div>
|
| 645 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Vehicle Type</label>
|
| 646 |
+
<select id="tab_vehicle" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-amber-500 focus:ring-2 focus:ring-amber-500/20 shadow-sm cursor-pointer">
|
| 647 |
+
<option value="Car">Car</option>
|
| 648 |
+
<option value="Truck">Truck</option>
|
| 649 |
+
<option value="Bus">Bus</option>
|
| 650 |
+
<option value="Motorcycle">Motorcycle</option>
|
| 651 |
+
</select>
|
| 652 |
+
</div>
|
| 653 |
+
</div>
|
| 654 |
+
</div>
|
| 655 |
+
|
| 656 |
+
<!-- Group 4: Driver Metrics Island -->
|
| 657 |
+
<div class="ios-glass-inner p-5 sm:p-6 rounded-[24px] shadow-sm border border-white/50 dark:border-white/10 hover:bg-white/50 dark:hover:bg-white/5 transition-colors group">
|
| 658 |
+
<div class="flex items-center gap-3 mb-4 border-b border-indigo-100/50 dark:border-white/10 pb-3">
|
| 659 |
+
<div class="w-8 h-8 rounded-xl bg-fuchsia-100 dark:bg-fuchsia-500/20 flex items-center justify-center border border-fuchsia-200/50 dark:border-fuchsia-400/30 group-hover:scale-110 transition-transform">
|
| 660 |
+
<i class="fas fa-user text-fuchsia-600 dark:text-fuchsia-300 text-xs"></i>
|
| 661 |
+
</div>
|
| 662 |
+
<h4 class="font-black text-[11px] uppercase tracking-widest text-indigo-900 dark:text-purple-200">Driver Profile</h4>
|
| 663 |
+
</div>
|
| 664 |
+
<div class="space-y-4">
|
| 665 |
+
<div>
|
| 666 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Driver Age</label>
|
| 667 |
+
<input type="number" id="tab_age" value="35" min="16" max="99" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 668 |
+
</div>
|
| 669 |
+
<div>
|
| 670 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Experience (Yrs)</label>
|
| 671 |
+
<input type="number" id="tab_exp" value="10" min="0" max="80" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 672 |
+
</div>
|
| 673 |
+
<div>
|
| 674 |
+
<label class="block text-[10px] font-black uppercase tracking-widest text-indigo-900/70 dark:text-purple-200/70 mb-1.5 ml-1">Alcohol Lvl (0-1)</label>
|
| 675 |
+
<input type="number" id="tab_alcohol" value="0.0" step="0.1" min="0" max="1" class="predict-input bg-white/60 dark:bg-black/20 focus:bg-white dark:focus:bg-black/40 border border-indigo-200/60 dark:border-white/10 text-indigo-950 dark:text-white focus:border-fuchsia-500 focus:ring-2 focus:ring-fuchsia-500/20 shadow-sm">
|
| 676 |
+
</div>
|
| 677 |
+
</div>
|
| 678 |
+
</div>
|
| 679 |
+
|
| 680 |
+
</div>
|
| 681 |
+
|
| 682 |
+
<!-- Submit Area -->
|
| 683 |
+
<div class="mt-8 pt-6 border-t border-indigo-200/50 dark:border-white/10 flex justify-end">
|
| 684 |
+
<button type="submit" id="predictBtn" class="w-full sm:w-auto px-8 py-4 rounded-2xl font-black uppercase tracking-widest text-xs bg-cyan-600 text-white shadow-lg shadow-cyan-500/40 hover:bg-cyan-500 active:scale-95 transition-all flex items-center justify-center gap-3">
|
| 685 |
+
<span>Run Risk Simulation</span>
|
| 686 |
+
<i id="predictSpinner" class="fas fa-circle-notch fa-spin hidden"></i>
|
| 687 |
+
</button>
|
| 688 |
+
</div>
|
| 689 |
+
</form>
|
| 690 |
+
</div>
|
| 691 |
+
|
| 692 |
+
<!-- Tabular Results View (Hidden by default) -->
|
| 693 |
+
<div id="predictiveResultView" class="hidden text-center animate-fade-in py-6">
|
| 694 |
+
<h3 class="text-xs font-black uppercase tracking-[0.3em] text-indigo-800/50 dark:text-purple-200 mb-6">Prediction Outcome</h3>
|
| 695 |
+
|
| 696 |
+
<div class="flex flex-col items-center justify-center gap-6">
|
| 697 |
+
<div id="riskStatusCard" class="ios-glass-inner p-10 rounded-[32px] shadow-sm dark:shadow-lg relative overflow-hidden w-full max-w-sm border border-indigo-200/50 dark:border-white/20">
|
| 698 |
+
<div id="riskBadge" class="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-black mb-6 uppercase tracking-[0.2em] border shadow-sm">
|
| 699 |
+
<i id="riskIcon" class="fas fa-check-circle"></i>
|
| 700 |
+
<span id="riskLabelText">Low Risk</span>
|
| 701 |
+
</div>
|
| 702 |
+
|
| 703 |
+
<h4 class="text-7xl font-black tracking-tighter text-indigo-950 dark:text-white mb-2 drop-shadow-sm"><span id="riskProbabilityValue">0</span>%</h4>
|
| 704 |
+
<p class="text-indigo-800/60 dark:text-purple-200 text-[10px] font-black uppercase tracking-widest mt-4">Calculated Accident Probability</p>
|
| 705 |
+
</div>
|
| 706 |
+
|
| 707 |
+
<div class="flex gap-4 max-w-sm w-full mt-4">
|
| 708 |
+
<button onclick="resetPredictModal()" class="flex-1 py-4 px-6 bg-cyan-600 text-white rounded-2xl font-black uppercase tracking-widest transition-all active:scale-95 text-xs flex justify-center items-center gap-2 shadow-xl shadow-cyan-500/30 hover:bg-cyan-500">
|
| 709 |
+
<i class="fas fa-redo text-white"></i> Retry
|
| 710 |
+
</button>
|
| 711 |
+
<button onclick="closePredictModal()" class="flex-1 py-4 px-6 bg-indigo-950 dark:bg-white/10 text-white rounded-2xl font-black uppercase tracking-widest transition-all active:scale-95 text-xs flex justify-center items-center gap-2 shadow-xl hover:opacity-90 border border-indigo-200/50 dark:border-white/20">
|
| 712 |
+
Dashboard
|
| 713 |
+
</button>
|
| 714 |
+
</div>
|
| 715 |
+
</div>
|
| 716 |
+
</div>
|
| 717 |
+
|
| 718 |
+
</div>
|
| 719 |
+
</div>
|
| 720 |
+
</div>
|
| 721 |
+
</div>
|
| 722 |
+
|
| 723 |
+
<script>
|
| 724 |
+
const themeToggleBtn = document.getElementById('themeToggle');
|
| 725 |
+
const htmlElement = document.documentElement;
|
| 726 |
+
|
| 727 |
+
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
| 728 |
+
htmlElement.classList.add('dark');
|
| 729 |
+
} else {
|
| 730 |
+
htmlElement.classList.remove('dark');
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
themeToggleBtn.addEventListener('click', () => {
|
| 734 |
+
htmlElement.classList.toggle('dark');
|
| 735 |
+
if (htmlElement.classList.contains('dark')) {
|
| 736 |
+
localStorage.theme = 'dark';
|
| 737 |
+
} else {
|
| 738 |
+
localStorage.theme = 'light';
|
| 739 |
+
}
|
| 740 |
+
});
|
| 741 |
+
|
| 742 |
+
let currentLocationStr = "Unknown Location";
|
| 743 |
+
|
| 744 |
+
async function fetchRealLocationData() {
|
| 745 |
+
if ("geolocation" in navigator) {
|
| 746 |
+
navigator.geolocation.getCurrentPosition(async (position) => {
|
| 747 |
+
const lat = position.coords.latitude;
|
| 748 |
+
const lon = position.coords.longitude;
|
| 749 |
+
|
| 750 |
+
try {
|
| 751 |
+
const geoRes = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`);
|
| 752 |
+
const geoData = await geoRes.json();
|
| 753 |
+
const city = geoData.address.city || geoData.address.town || geoData.address.village || geoData.address.county || "Unknown Region";
|
| 754 |
+
currentLocationStr = `${city}, ${geoData.address.country_code.toUpperCase()}`;
|
| 755 |
+
document.getElementById('realLocation').innerText = currentLocationStr;
|
| 756 |
+
} catch(e) { document.getElementById('realLocation').innerText = "Location API Error"; }
|
| 757 |
+
|
| 758 |
+
try {
|
| 759 |
+
const wxRes = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t_weather=true`);
|
| 760 |
+
const wxData = await wxRes.json();
|
| 761 |
+
const temp = wxData.current_weather.temperature;
|
| 762 |
+
document.getElementById('realWeather').innerText = `${temp}°C, Active`;
|
| 763 |
+
} catch(e) { document.getElementById('realWeather').innerText = "--"; }
|
| 764 |
+
}, (error) => {
|
| 765 |
+
document.getElementById('realLocation').innerText = "Location Denied";
|
| 766 |
+
document.getElementById('realWeather').innerText = "--";
|
| 767 |
+
});
|
| 768 |
+
} else {
|
| 769 |
+
document.getElementById('realLocation').innerText = "Not Supported";
|
| 770 |
+
}
|
| 771 |
+
const now = new Date();
|
| 772 |
+
document.getElementById('realTime').innerText = now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
fetchRealLocationData();
|
| 776 |
+
|
| 777 |
+
const fileInput = document.getElementById('fileInput');
|
| 778 |
+
const uploadSection = document.getElementById('uploadSection');
|
| 779 |
+
const resultsDiv = document.getElementById('results');
|
| 780 |
+
const loadingDiv = document.getElementById('loading');
|
| 781 |
+
const emergencyBanner = document.getElementById('emergencyBanner');
|
| 782 |
+
const ipCamModal = document.getElementById('ipCamModal');
|
| 783 |
+
const predictiveModal = document.getElementById('predictiveModal');
|
| 784 |
+
|
| 785 |
+
fileInput.addEventListener('change', () => {
|
| 786 |
+
if(fileInput.files.length > 0) {
|
| 787 |
+
handleUpload(fileInput.files[0]);
|
| 788 |
+
}
|
| 789 |
+
});
|
| 790 |
+
|
| 791 |
+
// --- IP Camera Modal Functions ---
|
| 792 |
+
function openIpCamModal() { ipCamModal.classList.remove('hidden'); }
|
| 793 |
+
function closeIpCamModal() {
|
| 794 |
+
ipCamModal.classList.add('hidden');
|
| 795 |
+
document.getElementById('ipCamUrl').value = '';
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
// --- Predictive Modal Functions ---
|
| 799 |
+
function openPredictModal() {
|
| 800 |
+
predictiveModal.classList.remove('hidden');
|
| 801 |
+
resetPredictModal(); // Ensure form shows up first
|
| 802 |
+
}
|
| 803 |
+
function closePredictModal() {
|
| 804 |
+
predictiveModal.classList.add('hidden');
|
| 805 |
+
}
|
| 806 |
+
function resetPredictModal() {
|
| 807 |
+
document.getElementById('predictiveFormView').classList.remove('hidden');
|
| 808 |
+
document.getElementById('predictiveResultView').classList.add('hidden');
|
| 809 |
+
}
|
| 810 |
+
|
| 811 |
+
async function submitPredictiveData(e) {
|
| 812 |
+
e.preventDefault();
|
| 813 |
+
|
| 814 |
+
const btn = document.getElementById('predictBtn');
|
| 815 |
+
const spinner = document.getElementById('predictSpinner');
|
| 816 |
+
|
| 817 |
+
btn.disabled = true;
|
| 818 |
+
spinner.classList.remove('hidden');
|
| 819 |
+
|
| 820 |
+
const payload = {
|
| 821 |
+
Weather: document.getElementById('tab_weather').value,
|
| 822 |
+
Road_Type: document.getElementById('tab_road_type').value,
|
| 823 |
+
Time_of_Day: document.getElementById('tab_time').value,
|
| 824 |
+
Traffic_Density: document.getElementById('tab_density').value,
|
| 825 |
+
Speed_Limit: document.getElementById('tab_speed').value,
|
| 826 |
+
Number_of_Vehicles: document.getElementById('tab_vehicles').value,
|
| 827 |
+
Driver_Alcohol: document.getElementById('tab_alcohol').value,
|
| 828 |
+
Road_Condition: document.getElementById('tab_road_cond').value,
|
| 829 |
+
Vehicle_Type: document.getElementById('tab_vehicle').value,
|
| 830 |
+
Driver_Age: document.getElementById('tab_age').value,
|
| 831 |
+
Driver_Experience: document.getElementById('tab_exp').value,
|
| 832 |
+
Road_Light_Condition: document.getElementById('tab_light').value
|
| 833 |
+
};
|
| 834 |
+
|
| 835 |
+
try {
|
| 836 |
+
const response = await fetch('/predict_traffic_risk', {
|
| 837 |
+
method: 'POST',
|
| 838 |
+
headers: { 'Content-Type': 'application/json' },
|
| 839 |
+
body: JSON.stringify(payload)
|
| 840 |
+
});
|
| 841 |
+
const data = await response.json();
|
| 842 |
+
|
| 843 |
+
if (data.error) throw new Error(data.error);
|
| 844 |
+
|
| 845 |
+
// Hide Form, Show Result
|
| 846 |
+
document.getElementById('predictiveFormView').classList.add('hidden');
|
| 847 |
+
document.getElementById('predictiveResultView').classList.remove('hidden');
|
| 848 |
+
|
| 849 |
+
const riskProb = data.risk_probability_percentage;
|
| 850 |
+
const willHappen = data.will_accident_happen;
|
| 851 |
+
|
| 852 |
+
const badge = document.getElementById('riskBadge');
|
| 853 |
+
const icon = document.getElementById('riskIcon');
|
| 854 |
+
const text = document.getElementById('riskLabelText');
|
| 855 |
+
|
| 856 |
+
text.innerText = data.status;
|
| 857 |
+
|
| 858 |
+
if (willHappen || riskProb >= 50) {
|
| 859 |
+
badge.className = "inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-black mb-6 uppercase tracking-[0.2em] border shadow-sm bg-red-100 text-red-600 border-red-300 dark:bg-red-500/20 dark:text-red-400 dark:border-red-500/30";
|
| 860 |
+
icon.className = "fas fa-exclamation-triangle";
|
| 861 |
+
} else {
|
| 862 |
+
badge.className = "inline-flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-black mb-6 uppercase tracking-[0.2em] border shadow-sm bg-emerald-100 text-emerald-600 border-emerald-300 dark:bg-emerald-500/20 dark:text-emerald-400 dark:border-emerald-500/30";
|
| 863 |
+
icon.className = "fas fa-shield-check";
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
animateValue("riskProbabilityValue", 0, Math.floor(riskProb), 1000);
|
| 867 |
+
|
| 868 |
+
} catch (err) {
|
| 869 |
+
alert(`Error: ${err.message}`);
|
| 870 |
+
resetPredictModal();
|
| 871 |
+
} finally {
|
| 872 |
+
btn.disabled = false;
|
| 873 |
+
spinner.classList.add('hidden');
|
| 874 |
+
}
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
async function connectIpCam() {
|
| 878 |
+
const url = document.getElementById('ipCamUrl').value.trim();
|
| 879 |
+
if(!url) return;
|
| 880 |
+
closeIpCamModal();
|
| 881 |
+
|
| 882 |
+
uploadSection.classList.add('hidden');
|
| 883 |
+
loadingDiv.classList.remove('hidden');
|
| 884 |
+
resultsDiv.classList.add('hidden');
|
| 885 |
+
emergencyBanner.classList.add('hidden');
|
| 886 |
+
|
| 887 |
+
document.getElementById('loadingIcon').className = "fas fa-satellite-dish text-2xl text-rose-600 dark:text-rose-400 animate-pulse";
|
| 888 |
+
document.getElementById('loadingText').innerText = "Connecting to IP Camera...";
|
| 889 |
+
document.getElementById('loadingSub').innerText = "Buffering 3s stream chunk...";
|
| 890 |
+
|
| 891 |
+
try {
|
| 892 |
+
const response = await fetch('/predict_stream', {
|
| 893 |
+
method: 'POST',
|
| 894 |
+
headers: { 'Content-Type': 'application/json' },
|
| 895 |
+
body: JSON.stringify({ url: url, location: currentLocationStr })
|
| 896 |
+
});
|
| 897 |
+
const data = await response.json();
|
| 898 |
+
if (data.error) throw new Error(data.error);
|
| 899 |
+
renderResultsUI(data);
|
| 900 |
+
} catch (err) {
|
| 901 |
+
alert(`Error: ${err.message}`);
|
| 902 |
+
resetApp();
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
async function handleUpload(file) {
|
| 907 |
+
const formData = new FormData();
|
| 908 |
+
formData.append('file', file);
|
| 909 |
+
formData.append('location', currentLocationStr);
|
| 910 |
+
|
| 911 |
+
uploadSection.classList.add('hidden');
|
| 912 |
+
loadingDiv.classList.add('hidden'); // Bypass the loading spinner
|
| 913 |
+
resultsDiv.classList.remove('hidden');
|
| 914 |
+
emergencyBanner.classList.add('hidden');
|
| 915 |
+
document.getElementById('mediaControls').classList.remove('hidden');
|
| 916 |
+
|
| 917 |
+
// Empty metadata for uploaded MP4s (cannot extract location/weather)
|
| 918 |
+
document.getElementById('realLocation').innerText = "--";
|
| 919 |
+
document.getElementById('realWeather').innerText = "--";
|
| 920 |
+
document.getElementById('realTime').innerText = "--";
|
| 921 |
+
|
| 922 |
+
// Initialize panel to 'Analyzing' state while video streams
|
| 923 |
+
document.getElementById('labelText').innerText = "Analyzing Video...";
|
| 924 |
+
document.getElementById('confidenceValue').innerText = "0";
|
| 925 |
+
document.getElementById('severityLevel').innerText = "--";
|
| 926 |
+
document.getElementById('slowfastScore').innerText = "--%";
|
| 927 |
+
document.getElementById('yoloScore').innerText = "--%";
|
| 928 |
+
document.getElementById('statusLine').style.backgroundColor = "#6366f1";
|
| 929 |
+
document.getElementById('statusBadge').style.color = "#6366f1";
|
| 930 |
+
document.getElementById('statusIcon').className = "fas fa-spinner fa-spin";
|
| 931 |
+
document.getElementById('alprContainer').innerHTML = '';
|
| 932 |
+
|
| 933 |
+
try {
|
| 934 |
+
// 1. Instantly upload and get Video ID
|
| 935 |
+
const upRes = await fetch('/upload_media', { method: 'POST', body: formData });
|
| 936 |
+
const upData = await upRes.json();
|
| 937 |
+
if (upData.error) throw new Error(upData.error);
|
| 938 |
+
|
| 939 |
+
window.currentVideoId = upData.video_id;
|
| 940 |
+
|
| 941 |
+
// 2. Start YOLO Stream Immediately
|
| 942 |
+
playTracking();
|
| 943 |
+
|
| 944 |
+
// 3. Kick off heavy prediction processing in the background
|
| 945 |
+
const anRes = await fetch('/analyze_media', {
|
| 946 |
+
method: 'POST',
|
| 947 |
+
headers: {'Content-Type': 'application/json'},
|
| 948 |
+
body: JSON.stringify({ video_id: upData.video_id })
|
| 949 |
+
});
|
| 950 |
+
const anData = await anRes.json();
|
| 951 |
+
if (anData.error) throw new Error(anData.error);
|
| 952 |
+
|
| 953 |
+
renderResultsUI(anData);
|
| 954 |
+
} catch (err) {
|
| 955 |
+
console.error(err);
|
| 956 |
+
alert("Processing failed: " + err.message);
|
| 957 |
+
resetApp();
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
// --- Media Player Functions ---
|
| 962 |
+
window.currentVideoId = null;
|
| 963 |
+
window.currentSnapshotData = null;
|
| 964 |
+
|
| 965 |
+
function showSnapshot() {
|
| 966 |
+
if(!window.currentSnapshotData) return;
|
| 967 |
+
document.getElementById('previewVideo').classList.add('hidden');
|
| 968 |
+
document.getElementById('previewVideo').pause();
|
| 969 |
+
const img = document.getElementById('previewImg');
|
| 970 |
+
img.classList.remove('hidden');
|
| 971 |
+
img.src = `data:image/jpeg;base64,${window.currentSnapshotData}`;
|
| 972 |
+
document.getElementById('liveBadge').innerHTML = '<span class="w-2 h-2 rounded-full bg-red-500 animate-pulse shadow-[0_0_8px_red]"></span> XAI SNAPSHOT';
|
| 973 |
+
}
|
| 974 |
+
|
| 975 |
+
function playTracking() {
|
| 976 |
+
document.getElementById('previewVideo').classList.add('hidden');
|
| 977 |
+
document.getElementById('previewVideo').pause();
|
| 978 |
+
const img = document.getElementById('previewImg');
|
| 979 |
+
img.classList.remove('hidden');
|
| 980 |
+
// Cache bust query parameter enforces a clean, immediate stream start
|
| 981 |
+
img.src = `/stream_tracking/${window.currentVideoId}?t=${new Date().getTime()}`;
|
| 982 |
+
document.getElementById('liveBadge').innerHTML = '<span class="w-2 h-2 rounded-full bg-fuchsia-500 animate-pulse shadow-[0_0_8px_fuchsia]"></span> LIVE AI TRACKING';
|
| 983 |
+
}
|
| 984 |
+
|
| 985 |
+
function playOriginal() {
|
| 986 |
+
document.getElementById('previewImg').classList.add('hidden');
|
| 987 |
+
document.getElementById('previewImg').src = "";
|
| 988 |
+
const video = document.getElementById('previewVideo');
|
| 989 |
+
video.classList.remove('hidden');
|
| 990 |
+
video.src = `/video/${window.currentVideoId}`;
|
| 991 |
+
video.play().catch(e => console.error("Playback error", e));
|
| 992 |
+
document.getElementById('liveBadge').innerHTML = '<span class="w-2 h-2 rounded-full bg-blue-500 shadow-[0_0_8px_blue]"></span> ORIGINAL VIDEO';
|
| 993 |
+
}
|
| 994 |
+
|
| 995 |
+
function renderResultsUI(data) {
|
| 996 |
+
window.currentVideoId = data.video_id;
|
| 997 |
+
if(data.image) window.currentSnapshotData = data.image;
|
| 998 |
+
|
| 999 |
+
// Retain IP location data if present (ignores uploads)
|
| 1000 |
+
if (data.cam_location) document.getElementById('realLocation').innerText = data.cam_location;
|
| 1001 |
+
if (data.cam_weather) document.getElementById('realWeather').innerText = data.cam_weather;
|
| 1002 |
+
|
| 1003 |
+
if (data.is_video) {
|
| 1004 |
+
document.getElementById('mediaControls').classList.remove('hidden');
|
| 1005 |
+
} else {
|
| 1006 |
+
document.getElementById('mediaControls').classList.add('hidden');
|
| 1007 |
+
}
|
| 1008 |
+
|
| 1009 |
+
document.getElementById('labelText').innerText = data.label;
|
| 1010 |
+
|
| 1011 |
+
const confValue = data.confidence;
|
| 1012 |
+
const sfScore = data.rcnn;
|
| 1013 |
+
const yScore = data.cnn;
|
| 1014 |
+
|
| 1015 |
+
document.getElementById('slowfastScore').innerText = sfScore + "%";
|
| 1016 |
+
document.getElementById('yoloScore').innerText = yScore + "%";
|
| 1017 |
+
|
| 1018 |
+
const statusLine = document.getElementById('statusLine');
|
| 1019 |
+
const badge = document.getElementById('statusBadge');
|
| 1020 |
+
const icon = document.getElementById('statusIcon');
|
| 1021 |
+
const isAccident = data.label.includes("Accident Detected");
|
| 1022 |
+
|
| 1023 |
+
if (isAccident) {
|
| 1024 |
+
statusLine.style.backgroundColor = "#ef4444";
|
| 1025 |
+
statusLine.style.boxShadow = "0 0 20px rgba(239,68,68,0.6)";
|
| 1026 |
+
badge.style.color = "#ef4444";
|
| 1027 |
+
badge.style.borderColor = "#fca5a5";
|
| 1028 |
+
icon.className = "fas fa-skull-crossbones";
|
| 1029 |
+
|
| 1030 |
+
const sevMatch = data.label.match(/\((.*?)\)/);
|
| 1031 |
+
if (sevMatch) {
|
| 1032 |
+
const sev = sevMatch[1];
|
| 1033 |
+
const severityContainer = document.getElementById('severityContainer');
|
| 1034 |
+
const severityLevel = document.getElementById('severityLevel');
|
| 1035 |
+
severityLevel.innerText = sev;
|
| 1036 |
+
severityContainer.classList.remove('hidden');
|
| 1037 |
+
if(sev.toLowerCase() === 'major') severityLevel.style.color = "#ef4444";
|
| 1038 |
+
if(sev.toLowerCase() === 'moderate') severityLevel.style.color = "#f97316";
|
| 1039 |
+
if(sev.toLowerCase() === 'minor') severityLevel.style.color = "#eab308";
|
| 1040 |
+
}
|
| 1041 |
+
} else {
|
| 1042 |
+
statusLine.style.backgroundColor = "#10b981";
|
| 1043 |
+
statusLine.style.boxShadow = "0 0 20px rgba(16,185,129,0.6)";
|
| 1044 |
+
badge.style.color = "#10b981";
|
| 1045 |
+
badge.style.borderColor = "#6ee7b7";
|
| 1046 |
+
icon.className = "fas fa-shield-check";
|
| 1047 |
+
document.getElementById('severityContainer').classList.add('hidden');
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
// Strictly enforcing empty ALPR panel if no valid plates exist
|
| 1051 |
+
const alprContainer = document.getElementById('alprContainer');
|
| 1052 |
+
alprContainer.innerHTML = '';
|
| 1053 |
+
if (data.plates && data.plates.length > 0) {
|
| 1054 |
+
const validPlates = data.plates.filter(p => p !== "NO CLEAR PLATES" && p.text !== "NO CLEAR PLATES");
|
| 1055 |
+
validPlates.forEach(plate => {
|
| 1056 |
+
const span = document.createElement('span');
|
| 1057 |
+
span.className = "bg-yellow-400 text-black px-3 py-1.5 rounded-xl font-black text-sm uppercase tracking-widest shadow-sm border border-yellow-500/50";
|
| 1058 |
+
span.innerText = plate.text || plate;
|
| 1059 |
+
alprContainer.appendChild(span);
|
| 1060 |
+
});
|
| 1061 |
+
}
|
| 1062 |
+
|
| 1063 |
+
if (data.alert_sent) emergencyBanner.classList.remove('hidden');
|
| 1064 |
+
|
| 1065 |
+
loadingDiv.classList.add('hidden');
|
| 1066 |
+
resultsDiv.classList.remove('hidden');
|
| 1067 |
+
|
| 1068 |
+
animateValue("confidenceValue", 0, Math.floor(confValue), 1200);
|
| 1069 |
+
setTimeout(() => {
|
| 1070 |
+
document.getElementById('slowfastBar').style.width = sfScore + '%';
|
| 1071 |
+
document.getElementById('yoloBar').style.width = yScore + '%';
|
| 1072 |
+
}, 200);
|
| 1073 |
+
}
|
| 1074 |
+
|
| 1075 |
+
function resetApp() {
|
| 1076 |
+
fileInput.value = '';
|
| 1077 |
+
document.getElementById('previewVideo').pause();
|
| 1078 |
+
document.getElementById('slowfastBar').style.width = '0%';
|
| 1079 |
+
document.getElementById('yoloBar').style.width = '0%';
|
| 1080 |
+
|
| 1081 |
+
emergencyBanner.classList.add('hidden');
|
| 1082 |
+
uploadSection.classList.remove('hidden');
|
| 1083 |
+
resultsDiv.classList.add('hidden');
|
| 1084 |
+
loadingDiv.classList.add('hidden');
|
| 1085 |
+
fetchRealLocationData(); // reset location back to user's browser location
|
| 1086 |
+
}
|
| 1087 |
+
|
| 1088 |
+
function animateValue(id, start, end, duration) {
|
| 1089 |
+
let current = start;
|
| 1090 |
+
const range = end - start;
|
| 1091 |
+
if(range === 0) { document.getElementById(id).innerHTML = end; return; }
|
| 1092 |
+
const increment = end > start ? 1 : -1;
|
| 1093 |
+
const stepTime = Math.abs(Math.floor(duration / range));
|
| 1094 |
+
const obj = document.getElementById(id);
|
| 1095 |
+
const timer = setInterval(function() {
|
| 1096 |
+
current += increment;
|
| 1097 |
+
obj.innerHTML = current;
|
| 1098 |
+
if ((increment > 0 && current >= end) || (increment < 0 && current <= end)) {
|
| 1099 |
+
clearInterval(timer);
|
| 1100 |
+
obj.innerHTML = end;
|
| 1101 |
+
}
|
| 1102 |
+
}, stepTime);
|
| 1103 |
+
}
|
| 1104 |
+
</script>
|
| 1105 |
+
</body>
|
| 1106 |
+
</html>
|