webapp1 commited on
Commit
b9a06a0
·
verified ·
1 Parent(s): 3980952

Upload 8 files

Browse files
.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}&current_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('&amp;', '&')
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

  • SHA256: 02e0dc2b6e23c72742c4c677f720df7d4084cfe03e6ee0703e72659c2e629825
  • Pointer size: 132 Bytes
  • Size of remote file: 1.25 MB
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}&current_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>