Raj Bhalerao commited on
Commit
3e04ea5
·
1 Parent(s): 2a7f65a

Release: Optimized Processing Engine

Browse files
Dockerfile CHANGED
@@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y \
6
  libgl1 \
7
  libglib2.0-0 \
8
  curl \
 
9
  && rm -rf /var/lib/apt/lists/*
10
 
11
  COPY requirements.txt .
 
6
  libgl1 \
7
  libglib2.0-0 \
8
  curl \
9
+ ffmpeg \
10
  && rm -rf /var/lib/apt/lists/*
11
 
12
  COPY requirements.txt .
backend/engine.py CHANGED
@@ -1,4 +1,6 @@
 
1
  import time
 
2
  import numpy as np
3
  import cv2
4
  from collections import defaultdict
@@ -17,19 +19,53 @@ def _point_to_segment_dist(px, py, ax, ay, bx, by):
17
  return np.linalg.norm(P - (A + t * AB))
18
 
19
 
20
- def run(model, video_path, line, config, on_frame):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  """
22
  Runs YOLO tracking on video. Calls on_frame(update_dict) after each processed frame.
23
  line: [[x1,y1], [x2,y2]]
 
24
  """
25
  cap = cv2.VideoCapture(video_path)
26
  fps = cap.get(cv2.CAP_PROP_FPS)
27
  total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
 
 
28
  cap.release()
29
 
30
  stride = config["detect_stride"]
31
  total_iters = total // stride
32
 
 
 
 
 
 
 
 
 
 
 
33
  prev_side = {}
34
  counted_ids = set()
35
  class_in = defaultdict(int)
@@ -57,6 +93,8 @@ def run(model, video_path, line, config, on_frame):
57
 
58
  for frame_idx, r in enumerate(results):
59
  active = 0
 
 
60
 
61
  if r.boxes.id is not None:
62
  ids = r.boxes.id.cpu().numpy()
@@ -67,6 +105,9 @@ def run(model, video_path, line, config, on_frame):
67
  confs = r.boxes.conf.cpu().numpy().tolist()
68
  conf_scores.extend(confs)
69
 
 
 
 
70
  for obj_id, c, box in zip(ids, cls, xyxy):
71
  cx = int((box[0] + box[2]) / 2)
72
  cy = int((box[1] + box[3]) / 2)
@@ -89,6 +130,12 @@ def run(model, video_path, line, config, on_frame):
89
 
90
  prev_side[obj_id] = current
91
 
 
 
 
 
 
 
92
  congestion.append(active)
93
 
94
  elapsed = time.time() - start
@@ -108,11 +155,14 @@ def run(model, video_path, line, config, on_frame):
108
 
109
  on_frame(update)
110
 
 
 
 
111
  processing_time = round(time.time() - start, 2)
112
  actual_fps = round(config["frames"] / processing_time, 2) if processing_time > 0 else 0
113
  speed_vs_rt = round(actual_fps / fps, 2) if fps > 0 else 0
114
 
115
- return {
116
  "class_in": dict(class_in),
117
  "class_out": dict(class_out),
118
  "congestion": congestion,
@@ -122,3 +172,8 @@ def run(model, video_path, line, config, on_frame):
122
  "actual_fps": actual_fps,
123
  "speed_vs_realtime": speed_vs_rt,
124
  }
 
 
 
 
 
 
1
+ import os
2
  import time
3
+ import tempfile
4
  import numpy as np
5
  import cv2
6
  from collections import defaultdict
 
19
  return np.linalg.norm(P - (A + t * AB))
20
 
21
 
22
+ # Lightweight drawing colors (BGR for OpenCV)
23
+ _CLR_BOX = (230, 180, 50) # teal-ish
24
+ _CLR_LINE = (80, 220, 100) # green
25
+ _CLR_TEXT_BG = (30, 30, 30) # dark bg for text
26
+
27
+
28
+ def _draw_annotations(frame, boxes, ids, line_pts):
29
+ """Draw bounding boxes, track IDs, and counting line on frame in-place."""
30
+ # Counting line
31
+ cv2.line(frame, tuple(line_pts[0]), tuple(line_pts[1]), _CLR_LINE, 3, cv2.LINE_AA)
32
+
33
+ if boxes is not None and ids is not None:
34
+ for box, obj_id in zip(boxes, ids):
35
+ x1, y1, x2, y2 = map(int, box)
36
+ cv2.rectangle(frame, (x1, y1), (x2, y2), _CLR_BOX, 2)
37
+ label = f"ID:{int(obj_id)}"
38
+ (tw, th), _ = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.4, 1)
39
+ cv2.rectangle(frame, (x1, y1 - th - 6), (x1 + tw + 6, y1), _CLR_TEXT_BG, -1)
40
+ cv2.putText(frame, label, (x1 + 3, y1 - 4), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 1, cv2.LINE_AA)
41
+
42
+
43
+ def run(model, video_path, line, config, on_frame, save_annotated=False):
44
  """
45
  Runs YOLO tracking on video. Calls on_frame(update_dict) after each processed frame.
46
  line: [[x1,y1], [x2,y2]]
47
+ save_annotated: if True, writes annotated MP4 with boxes + IDs + counting line
48
  """
49
  cap = cv2.VideoCapture(video_path)
50
  fps = cap.get(cv2.CAP_PROP_FPS)
51
  total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
52
+ out_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
53
+ out_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
54
  cap.release()
55
 
56
  stride = config["detect_stride"]
57
  total_iters = total // stride
58
 
59
+ # Annotated video writer (temp directory — auto-cleaned on container shutdown)
60
+ annotated_path = None
61
+ writer = None
62
+ if save_annotated:
63
+ annotated_dir = os.path.join(tempfile.gettempdir(), "funky_reports")
64
+ os.makedirs(annotated_dir, exist_ok=True)
65
+ annotated_path = os.path.join(annotated_dir, "annotated.mp4")
66
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
67
+ writer = cv2.VideoWriter(annotated_path, fourcc, fps / stride, (out_w, out_h))
68
+
69
  prev_side = {}
70
  counted_ids = set()
71
  class_in = defaultdict(int)
 
93
 
94
  for frame_idx, r in enumerate(results):
95
  active = 0
96
+ cur_boxes = None
97
+ cur_ids = None
98
 
99
  if r.boxes.id is not None:
100
  ids = r.boxes.id.cpu().numpy()
 
105
  confs = r.boxes.conf.cpu().numpy().tolist()
106
  conf_scores.extend(confs)
107
 
108
+ cur_boxes = xyxy
109
+ cur_ids = ids
110
+
111
  for obj_id, c, box in zip(ids, cls, xyxy):
112
  cx = int((box[0] + box[2]) / 2)
113
  cy = int((box[1] + box[3]) / 2)
 
130
 
131
  prev_side[obj_id] = current
132
 
133
+ # Write annotated frame (only for frames we already process)
134
+ if writer is not None:
135
+ frame = r.orig_img.copy()
136
+ _draw_annotations(frame, cur_boxes, cur_ids, [a, b])
137
+ writer.write(frame)
138
+
139
  congestion.append(active)
140
 
141
  elapsed = time.time() - start
 
155
 
156
  on_frame(update)
157
 
158
+ if writer is not None:
159
+ writer.release()
160
+
161
  processing_time = round(time.time() - start, 2)
162
  actual_fps = round(config["frames"] / processing_time, 2) if processing_time > 0 else 0
163
  speed_vs_rt = round(actual_fps / fps, 2) if fps > 0 else 0
164
 
165
+ result = {
166
  "class_in": dict(class_in),
167
  "class_out": dict(class_out),
168
  "congestion": congestion,
 
172
  "actual_fps": actual_fps,
173
  "speed_vs_realtime": speed_vs_rt,
174
  }
175
+
176
+ if annotated_path and os.path.exists(annotated_path):
177
+ result["annotated_video"] = annotated_path
178
+
179
+ return result
backend/server.py CHANGED
@@ -79,7 +79,16 @@ def generate_reports(video_id: str):
79
  if not data:
80
  return {"error": "no results", "files": []}
81
  out_dir = str(REPORT_DIR / video_id)
82
- files = generate_all(data, MODEL_CLASSES, out_dir)
 
 
 
 
 
 
 
 
 
83
  return {"files": files}
84
 
85
 
@@ -88,7 +97,12 @@ def get_report(video_id: str, name: str):
88
  path = REPORT_DIR / video_id / name
89
  if not path.exists():
90
  return Response(status_code=404)
91
- return FileResponse(str(path), media_type="image/png")
 
 
 
 
 
92
 
93
 
94
  @app.websocket("/ws/run")
@@ -99,6 +113,8 @@ async def ws_run(ws: WebSocket):
99
  video_id = data["video_id"]
100
  line = data["line"]
101
  cfg = data["config"]
 
 
102
 
103
  path = videos.get(video_id)
104
 
@@ -109,7 +125,9 @@ async def ws_run(ws: WebSocket):
109
  def on_frame(update):
110
  loop.call_soon_threadsafe(queue.put_nowait, update)
111
 
112
- task = loop.run_in_executor(None, run, model, path, line, cfg, on_frame)
 
 
113
 
114
  try:
115
  while True:
@@ -124,6 +142,7 @@ async def ws_run(ws: WebSocket):
124
  await asyncio.sleep(0.05)
125
 
126
  result = task.result()
 
127
  run_results[video_id] = result
128
  await ws.send_text(json.dumps({
129
  "done": True,
 
79
  if not data:
80
  return {"error": "no results", "files": []}
81
  out_dir = str(REPORT_DIR / video_id)
82
+ report_format = data.get("report_format", "png")
83
+ files = generate_all(data, MODEL_CLASSES, out_dir, report_format)
84
+ # Include annotated video if it exists
85
+ annotated_src = data.get("annotated_video")
86
+ if annotated_src and os.path.exists(annotated_src):
87
+ import shutil
88
+ dest = os.path.join(out_dir, "annotated.mp4")
89
+ if not os.path.exists(dest):
90
+ shutil.copy2(annotated_src, dest)
91
+ files.append("annotated.mp4")
92
  return {"files": files}
93
 
94
 
 
97
  path = REPORT_DIR / video_id / name
98
  if not path.exists():
99
  return Response(status_code=404)
100
+ media = "image/png"
101
+ if name.endswith(".pdf"):
102
+ media = "application/pdf"
103
+ elif name.endswith(".mp4"):
104
+ media = "video/mp4"
105
+ return FileResponse(str(path), media_type=media)
106
 
107
 
108
  @app.websocket("/ws/run")
 
113
  video_id = data["video_id"]
114
  line = data["line"]
115
  cfg = data["config"]
116
+ save_annotated = data.get("annotated_video", False)
117
+ report_format = data.get("report_format", "png")
118
 
119
  path = videos.get(video_id)
120
 
 
125
  def on_frame(update):
126
  loop.call_soon_threadsafe(queue.put_nowait, update)
127
 
128
+ task = loop.run_in_executor(
129
+ None, run, model, path, line, cfg, on_frame, save_annotated
130
+ )
131
 
132
  try:
133
  while True:
 
142
  await asyncio.sleep(0.05)
143
 
144
  result = task.result()
145
+ result["report_format"] = report_format
146
  run_results[video_id] = result
147
  await ws.send_text(json.dumps({
148
  "done": True,
backend/visualize.py CHANGED
@@ -32,12 +32,14 @@ def _style(ax, title, xlabel="", ylabel=""):
32
  ax.set_axisbelow(True)
33
 
34
 
35
- def _save(fig, path):
 
 
36
  fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
37
  plt.close(fig)
38
 
39
 
40
- def direction_pie(total_in, total_out, out_dir):
41
  if total_in + total_out == 0:
42
  return None
43
  fig, ax = plt.subplots(figsize=(5, 5), facecolor=C_BG)
@@ -47,7 +49,7 @@ def direction_pie(total_in, total_out, out_dir):
47
  autopct="%1.1f%%",
48
  startangle=90,
49
  colors=[C_IN, C_OUT],
50
- wedgeprops={"edgecolor": C_BG, "linewidth": 2.5},
51
  textprops={"fontsize": 10, "fontweight": "600", "color": C_PRIMARY},
52
  )
53
  for t in autotexts:
@@ -58,11 +60,12 @@ def direction_pie(total_in, total_out, out_dir):
58
  total = total_in + total_out
59
  ax.text(0, -1.35, f"Total: {total} vehicles", ha="center", fontsize=9, color=C_ACCENT, fontweight="500")
60
  path = os.path.join(out_dir, "direction_pie.png")
61
- _save(fig, path)
62
- return "direction_pie.png"
 
63
 
64
 
65
- def flow_histogram(flow_times, out_dir):
66
  if not flow_times:
67
  return None
68
  fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
@@ -76,11 +79,12 @@ def flow_histogram(flow_times, out_dir):
76
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
77
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
78
  path = os.path.join(out_dir, "flow_over_time.png")
79
- _save(fig, path)
80
- return "flow_over_time.png"
 
81
 
82
 
83
- def congestion_chart(congestion, out_dir):
84
  if not congestion:
85
  return None
86
  fig, ax = plt.subplots(figsize=(10, 4), facecolor=C_BG)
@@ -99,11 +103,12 @@ def congestion_chart(congestion, out_dir):
99
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
100
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
101
  path = os.path.join(out_dir, "congestion_index.png")
102
- _save(fig, path)
103
- return "congestion_index.png"
 
104
 
105
 
106
- def class_dominance(class_in, class_out, model_classes, out_dir):
107
  totals = {}
108
  for k in set(list(class_in.keys()) + list(class_out.keys())):
109
  totals[k] = class_in.get(k, 0) + class_out.get(k, 0)
@@ -129,11 +134,12 @@ def class_dominance(class_in, class_out, model_classes, out_dir):
129
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
130
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
131
  path = os.path.join(out_dir, "class_dominance.png")
132
- _save(fig, path)
133
- return "class_dominance.png"
 
134
 
135
 
136
- def confidence_dist(conf_scores, out_dir):
137
  if not conf_scores:
138
  return None
139
  fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
@@ -147,11 +153,12 @@ def confidence_dist(conf_scores, out_dir):
147
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
148
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
149
  path = os.path.join(out_dir, "confidence_dist.png")
150
- _save(fig, path)
151
- return "confidence_dist.png"
 
152
 
153
 
154
- def generate_all(data, model_classes, out_dir):
155
  os.makedirs(out_dir, exist_ok=True)
156
 
157
  plt.rcParams.update({
@@ -163,13 +170,15 @@ def generate_all(data, model_classes, out_dir):
163
  total_in = sum(data["class_in"].values())
164
  total_out = sum(data["class_out"].values())
165
 
 
 
166
  files = []
167
  for fn in [
168
- lambda: direction_pie(total_in, total_out, out_dir),
169
- lambda: flow_histogram(data.get("flow_times", []), out_dir),
170
- lambda: congestion_chart(data.get("congestion", []), out_dir),
171
- lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir),
172
- lambda: confidence_dist(data.get("conf_scores", []), out_dir),
173
  ]:
174
  name = fn()
175
  if name:
 
32
  ax.set_axisbelow(True)
33
 
34
 
35
+ def _save(fig, path, fmt="png"):
36
+ if fmt == "pdf":
37
+ path = path.rsplit(".", 1)[0] + ".pdf"
38
  fig.savefig(path, dpi=200, bbox_inches="tight", facecolor=C_BG, edgecolor="none")
39
  plt.close(fig)
40
 
41
 
42
+ def direction_pie(total_in, total_out, out_dir, fmt="png"):
43
  if total_in + total_out == 0:
44
  return None
45
  fig, ax = plt.subplots(figsize=(5, 5), facecolor=C_BG)
 
49
  autopct="%1.1f%%",
50
  startangle=90,
51
  colors=[C_IN, C_OUT],
52
+ wedgeprops={"edgecolor": C_BG, "linewidth": 2.5 if (total_in > 0 and total_out > 0) else 0},
53
  textprops={"fontsize": 10, "fontweight": "600", "color": C_PRIMARY},
54
  )
55
  for t in autotexts:
 
60
  total = total_in + total_out
61
  ax.text(0, -1.35, f"Total: {total} vehicles", ha="center", fontsize=9, color=C_ACCENT, fontweight="500")
62
  path = os.path.join(out_dir, "direction_pie.png")
63
+ _save(fig, path, fmt)
64
+ ext = fmt if fmt == "pdf" else "png"
65
+ return f"direction_pie.{ext}"
66
 
67
 
68
+ def flow_histogram(flow_times, out_dir, fmt="png"):
69
  if not flow_times:
70
  return None
71
  fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
 
79
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
80
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
81
  path = os.path.join(out_dir, "flow_over_time.png")
82
+ _save(fig, path, fmt)
83
+ ext = fmt if fmt == "pdf" else "png"
84
+ return f"flow_over_time.{ext}"
85
 
86
 
87
+ def congestion_chart(congestion, out_dir, fmt="png"):
88
  if not congestion:
89
  return None
90
  fig, ax = plt.subplots(figsize=(10, 4), facecolor=C_BG)
 
103
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
104
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
105
  path = os.path.join(out_dir, "congestion_index.png")
106
+ _save(fig, path, fmt)
107
+ ext = fmt if fmt == "pdf" else "png"
108
+ return f"congestion_index.{ext}"
109
 
110
 
111
+ def class_dominance(class_in, class_out, model_classes, out_dir, fmt="png"):
112
  totals = {}
113
  for k in set(list(class_in.keys()) + list(class_out.keys())):
114
  totals[k] = class_in.get(k, 0) + class_out.get(k, 0)
 
134
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
135
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
136
  path = os.path.join(out_dir, "class_dominance.png")
137
+ _save(fig, path, fmt)
138
+ ext = fmt if fmt == "pdf" else "png"
139
+ return f"class_dominance.{ext}"
140
 
141
 
142
+ def confidence_dist(conf_scores, out_dir, fmt="png"):
143
  if not conf_scores:
144
  return None
145
  fig, ax = plt.subplots(figsize=(9, 4), facecolor=C_BG)
 
153
  transform=ax.transAxes, ha="right", va="top", fontsize=8, color=C_ACCENT,
154
  bbox=dict(boxstyle="round,pad=0.4", facecolor="#f8fafc", edgecolor=C_GRID))
155
  path = os.path.join(out_dir, "confidence_dist.png")
156
+ _save(fig, path, fmt)
157
+ ext = fmt if fmt == "pdf" else "png"
158
+ return f"confidence_dist.{ext}"
159
 
160
 
161
+ def generate_all(data, model_classes, out_dir, report_format="png"):
162
  os.makedirs(out_dir, exist_ok=True)
163
 
164
  plt.rcParams.update({
 
170
  total_in = sum(data["class_in"].values())
171
  total_out = sum(data["class_out"].values())
172
 
173
+ fmt = report_format
174
+
175
  files = []
176
  for fn in [
177
+ lambda: direction_pie(total_in, total_out, out_dir, fmt),
178
+ lambda: flow_histogram(data.get("flow_times", []), out_dir, fmt),
179
+ lambda: congestion_chart(data.get("congestion", []), out_dir, fmt),
180
+ lambda: class_dominance(data["class_in"], data["class_out"], model_classes, out_dir, fmt),
181
+ lambda: confidence_dist(data.get("conf_scores", []), out_dir, fmt),
182
  ]:
183
  name = fn()
184
  if name:
frontend/initial.html CHANGED
@@ -4,7 +4,7 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>UrbanFlow - Enterprise Setup</title>
8
  <link rel="icon" type="image/svg+xml" href="uf.svg">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@@ -14,7 +14,7 @@
14
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
15
  <style>
16
  body {
17
- font-family: 'Inter', sans-serif;
18
  }
19
 
20
  .font-montserrat {
@@ -62,13 +62,6 @@
62
 
63
  <header class="mt-8 flex flex-col items-center flex-shrink-0 w-full z-10">
64
  <img src="uf_logo.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
65
- <div class="flex items-center space-x-3">
66
- <span class="w-12 h-[1px] bg-slate-200"></span>
67
- <p class="font-montserrat font-bold tracking-[0.25em] uppercase text-[10px] text-slate-400 text-center">
68
- Enterprise AI Pipeline
69
- </p>
70
- <span class="w-12 h-[1px] bg-slate-200"></span>
71
- </div>
72
  </header>
73
 
74
  <main
@@ -82,7 +75,7 @@
82
  <p
83
  class="font-montserrat font-bold mb-8 text-sm uppercase tracking-[0.2em] text-slate-400 flex items-center">
84
  <span class="bg-slate-100 text-slate-600 px-3 py-1 rounded-full text-[10px] mr-3">v1.0 CORE</span>
85
- Powered by Deep Learning Inference
86
  </p>
87
  <ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-montserrat font-medium text-slate-700">
88
  <li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Real-time
@@ -104,59 +97,25 @@
104
  class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
105
 
106
  <!-- STEP: Module Select -->
107
- <div id="step-modules" class="w-full flex flex-col fade-in justify-center">
108
- <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Select AI Module</h2>
109
- <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-6 text-center">Choose an intelligence pipeline for
110
- your media
111
- stream.</p>
112
 
113
- <div class="grid grid-cols-2 gap-4">
114
  <div onclick="showStep('upload')"
115
- class="group relative bg-white border-2 border-slate-900 rounded-[1.5rem] p-6 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 text-center">
116
  <div
117
- class="absolute top-4 right-4 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
118
  Active</div>
119
- <i class="fa-solid fa-car-side text-3xl text-slate-900 mb-4 block mx-auto"></i>
120
- <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight">Traffic <br>Analytics</h3>
121
- <p class="text-[10px] text-slate-500 font-montserrat font-medium">Detect, track, and analyze vehicles in
 
 
122
  real-world environments using state-of-the-art vision models.</p>
123
  </div>
124
- <div
125
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
126
- <div
127
- class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
128
- Coming Soon</div>
129
- <i class="fa-solid fa-layer-group text-3xl text-slate-300 mb-4 block mx-auto"></i>
130
- <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Semantic
131
- <br>Segmentation
132
- </h3>
133
- <p class="text-[10px] text-slate-400 font-montserrat font-medium">Pixel-perfect instance segmentation for
134
- complex spatial scene understanding.</p>
135
- </div>
136
- <div
137
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
138
- <div
139
- class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
140
- Coming Soon</div>
141
- <i class="fa-solid fa-tags text-3xl text-slate-300 mb-4 block mx-auto"></i>
142
- <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Image
143
- <br>Classification
144
- </h3>
145
- <p class="text-[10px] text-slate-400 font-montserrat font-medium">High-speed categorical labeling for vast and
146
- diverse image datasets.</p>
147
- </div>
148
- <div
149
- class="relative bg-slate-50 border border-slate-100 rounded-[1.5rem] p-6 opacity-60 cursor-not-allowed text-center">
150
- <div
151
- class="absolute top-4 right-4 bg-white border border-slate-200 text-slate-400 text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
152
- Coming Soon</div>
153
- <i class="fa-solid fa-expand text-3xl text-slate-300 mb-4 block mx-auto"></i>
154
- <h3 class="font-montserrat font-bold text-sm mb-2 leading-tight text-slate-700">Custom
155
- <br>Detection
156
- </h3>
157
- <p class="text-[10px] text-slate-400 font-montserrat font-medium">Deploy proprietary neural networks for highly
158
- specialized edge inference.</p>
159
- </div>
160
  </div>
161
  </div>
162
 
@@ -164,13 +123,14 @@
164
  <div id="step-upload" class="hidden w-full flex flex-col fade-in justify-center">
165
  <button onclick="showStep('modules')"
166
  class="text-slate-400 hover:text-black transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
167
- <i class="fa-solid fa-arrow-left mr-2"></i> Back to Modules
168
  </button>
169
- <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Initialize Media Source
170
  </h2>
171
- <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-8 text-center">Provide the target video footage to
 
172
  configure the
173
- Traffic Analytics pipeline.</p>
174
 
175
  <div id="dropzone"
176
  class="border border-dashed border-slate-300 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer hover:border-black hover:bg-slate-50 transition-all duration-300 group">
@@ -196,21 +156,7 @@
196
  </div>
197
  </div>
198
 
199
- <!-- STEP: Config -->
200
- <div id="step-config" class="hidden w-full flex flex-col fade-in justify-center">
201
- <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">System Configuration</h2>
202
- <p
203
- class="text-[11px] font-montserrat font-bold text-slate-400 mb-8 uppercase tracking-widest text-center">
204
- Optimal values auto-configured for performance
205
- </p>
206
-
207
- <div class="space-y-4 xl:space-y-5 mb-8" id="config-fields"></div>
208
 
209
- <button onclick="showStep('draw')"
210
- class="w-full py-3.5 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-base xl:text-lg flex justify-center items-center">
211
- Continue <i class="fa-solid fa-arrow-right ml-3 text-sm"></i>
212
- </button>
213
- </div>
214
 
215
  <!-- STEP: Draw Line -->
216
  <div id="step-draw" class="hidden w-full flex flex-col fade-in justify-center">
@@ -239,8 +185,8 @@
239
  Reset
240
  </button>
241
  <button id="btn-proceed" onclick="startRun()"
242
- class="w-2/3 py-3.5 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-center text-sm xl:text-base">
243
- Proceed
244
  </button>
245
  </div>
246
  </div>
@@ -252,20 +198,12 @@
252
  let videoId = null;
253
  let runConfig = {};
254
 
255
- const configParams = [
256
- { key: "imgsz", label: "Image Size", step: 32, min: 640, max: 1280 },
257
- { key: "conf", label: "Confidence", step: 0.01, min: 0.08, max: 1.0, decimals: 2 },
258
- { key: "iou", label: "IoU Threshold", step: 0.05, min: 0.5, max: 1.0, decimals: 2 },
259
- { key: "detect_stride", label: "Frame Stride", step: 1, min: 1, max: 10 }
260
- ];
261
-
262
  function showStep(name) {
263
- ['modules', 'upload', 'config', 'draw'].forEach(s => {
264
  document.getElementById('step-' + s).classList.add('hidden');
265
  });
266
  document.getElementById('step-' + name).classList.remove('hidden');
267
 
268
- if (name === 'config') renderConfig();
269
  if (name === 'draw') loadFirstFrame();
270
  }
271
 
@@ -319,53 +257,14 @@
319
  runConfig.conf = 0.12;
320
  runConfig.iou = 0.60;
321
  txt.innerText = 'Initialization Complete';
322
- setTimeout(() => showStep('config'), 400);
323
  });
324
  };
325
 
326
  xhr.send(form);
327
  }
328
 
329
- // Config
330
- function renderConfig() {
331
- const container = document.getElementById('config-fields');
332
- container.innerHTML = '';
333
-
334
- configParams.forEach(p => {
335
- let val = runConfig[p.key];
336
- const d = p.decimals || 0;
337
-
338
- const row = document.createElement('div');
339
- row.className = 'flex items-center justify-between border-b border-slate-100 pb-3';
340
- row.innerHTML = `
341
- <span class="font-montserrat font-medium text-slate-700 text-sm xl:text-base">${p.label}</span>
342
- <div class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
343
- <button class="text-slate-400 hover:text-black transition p-1" data-dir="-1" data-key="${p.key}">
344
- <i class="fa-solid fa-chevron-left text-[10px]"></i>
345
- </button>
346
- <span class="font-mono font-bold w-12 text-center text-slate-900 text-base" id="cfg-${p.key}">${d ? val.toFixed(d) : val}</span>
347
- <button class="text-slate-400 hover:text-black transition p-1" data-dir="1" data-key="${p.key}">
348
- <i class="fa-solid fa-chevron-right text-[10px]"></i>
349
- </button>
350
- </div>
351
- `;
352
- container.appendChild(row);
353
- });
354
 
355
- container.querySelectorAll('button').forEach(btn => {
356
- btn.addEventListener('click', () => {
357
- const key = btn.dataset.key;
358
- const dir = parseInt(btn.dataset.dir);
359
- const param = configParams.find(p => p.key === key);
360
- let val = runConfig[key] + dir * param.step;
361
- val = Math.max(param.min, Math.min(param.max, val));
362
- val = param.decimals ? parseFloat(val.toFixed(param.decimals)) : Math.round(val);
363
- runConfig[key] = val;
364
- const d = param.decimals || 0;
365
- document.getElementById('cfg-' + key).innerText = d ? val.toFixed(d) : val;
366
- });
367
- });
368
- }
369
 
370
  // Draw line
371
  const canvas = document.getElementById('drawing-canvas');
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>UrbanFlow</title>
8
  <link rel="icon" type="image/svg+xml" href="uf.svg">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
 
14
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
15
  <style>
16
  body {
17
+ font-family: 'Montserrat', sans-serif;
18
  }
19
 
20
  .font-montserrat {
 
62
 
63
  <header class="mt-8 flex flex-col items-center flex-shrink-0 w-full z-10">
64
  <img src="uf_logo.png" alt="UrbanFlow Logo" class="h-44 md:h-52 w-auto object-contain mb-3">
 
 
 
 
 
 
 
65
  </header>
66
 
67
  <main
 
75
  <p
76
  class="font-montserrat font-bold mb-8 text-sm uppercase tracking-[0.2em] text-slate-400 flex items-center">
77
  <span class="bg-slate-100 text-slate-600 px-3 py-1 rounded-full text-[10px] mr-3">v1.0 CORE</span>
78
+ Powered by High-Density Vision Metrics
79
  </p>
80
  <ul class="space-y-4 xl:space-y-5 text-base xl:text-lg font-montserrat font-medium text-slate-700">
81
  <li class="flex items-center"><i class="fa-solid fa-check text-black mr-5 text-xl"></i> Real-time
 
97
  class="lg:col-span-5 flex flex-col justify-center w-full max-w-[32rem] mx-auto min-h-[450px] mb-12 lg:mb-0">
98
 
99
  <!-- STEP: Module Select -->
100
+ <div id="step-modules" class="w-full flex flex-col fade-in justify-center items-center">
101
+ <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">UrbanFlow
102
+ </h2>
103
+ <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-10 text-center">Active Analytical
104
+ Environment: Traffic Dynamics</p>
105
 
106
+ <div class="flex justify-center w-full">
107
  <div onclick="showStep('upload')"
108
+ class="group relative bg-white border-2 border-slate-900 rounded-[2rem] p-8 cursor-pointer hover:shadow-2xl hover:-translate-y-1 transition-all duration-300 text-center max-w-sm w-full">
109
  <div
110
+ class="absolute top-4 right-6 bg-slate-900 text-white text-[9px] font-bold px-2.5 py-1 rounded-full uppercase tracking-wider">
111
  Active</div>
112
+ <i class="fa-solid fa-car-side text-4xl text-slate-900 mb-4 block mx-auto"></i>
113
+ <h3 class="font-montserrat font-bold text-lg mb-2 leading-tight">Traffic <br>Dynamics</h3>
114
+ <p class="text-[10px] text-slate-500 font-montserrat font-medium leading-relaxed">Detect, track,
115
+ and analyze
116
+ vehicles in
117
  real-world environments using state-of-the-art vision models.</p>
118
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  </div>
120
  </div>
121
 
 
123
  <div id="step-upload" class="hidden w-full flex flex-col fade-in justify-center">
124
  <button onclick="showStep('modules')"
125
  class="text-slate-400 hover:text-black transition flex items-center text-xs font-bold uppercase tracking-widest mb-6 w-fit">
126
+ <i class="fa-solid fa-arrow-left mr-2"></i> Back to Suite
127
  </button>
128
+ <h2 class="text-3xl font-montserrat font-bold mb-2 text-slate-900 text-center">Define Media Source
129
  </h2>
130
+ <p class="text-[13px] font-montserrat font-medium text-slate-400 mb-8 text-center">Provide the target
131
+ video footage to
132
  configure the
133
+ analytical pipeline.</p>
134
 
135
  <div id="dropzone"
136
  class="border border-dashed border-slate-300 rounded-[2rem] p-12 flex flex-col items-center justify-center cursor-pointer hover:border-black hover:bg-slate-50 transition-all duration-300 group">
 
156
  </div>
157
  </div>
158
 
 
 
 
 
 
 
 
 
 
159
 
 
 
 
 
 
160
 
161
  <!-- STEP: Draw Line -->
162
  <div id="step-draw" class="hidden w-full flex flex-col fade-in justify-center">
 
185
  Reset
186
  </button>
187
  <button id="btn-proceed" onclick="startRun()"
188
+ class="w-2/3 py-3.5 bg-black text-white rounded-full font-montserrat font-medium hover:bg-slate-800 transition-all text-center text-sm xl:text-base">
189
+ Continue &nbsp;&rarr;
190
  </button>
191
  </div>
192
  </div>
 
198
  let videoId = null;
199
  let runConfig = {};
200
 
 
 
 
 
 
 
 
201
  function showStep(name) {
202
+ ['modules', 'upload', 'draw'].forEach(s => {
203
  document.getElementById('step-' + s).classList.add('hidden');
204
  });
205
  document.getElementById('step-' + name).classList.remove('hidden');
206
 
 
207
  if (name === 'draw') loadFirstFrame();
208
  }
209
 
 
257
  runConfig.conf = 0.12;
258
  runConfig.iou = 0.60;
259
  txt.innerText = 'Initialization Complete';
260
+ setTimeout(() => showStep('draw'), 400);
261
  });
262
  };
263
 
264
  xhr.send(form);
265
  }
266
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
  // Draw line
270
  const canvas = document.getElementById('drawing-canvas');
frontend/settings.html ADDED
@@ -0,0 +1,426 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>UrbanFlow</title>
8
+ <link rel="icon" type="image/svg+xml" href="uf.svg">
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
11
+ <link
12
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@400;500;600;700;800;900&display=swap"
13
+ rel="stylesheet">
14
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
15
+ <style>
16
+ body {
17
+ font-family: 'Inter', sans-serif;
18
+ }
19
+
20
+ .font-montserrat {
21
+ font-family: 'Montserrat', sans-serif;
22
+ }
23
+
24
+ .mono-font {
25
+ font-family: 'JetBrains Mono', monospace;
26
+ }
27
+
28
+ .fade-in {
29
+ animation: fadeIn 0.4s ease-in-out forwards;
30
+ }
31
+
32
+ @keyframes fadeIn {
33
+ from {
34
+ opacity: 0;
35
+ transform: translateY(10px);
36
+ }
37
+
38
+ to {
39
+ opacity: 1;
40
+ transform: translateY(0);
41
+ }
42
+ }
43
+
44
+ .bg-glow {
45
+ position: absolute;
46
+ width: 600px;
47
+ height: 600px;
48
+ background: radial-gradient(circle, rgba(241, 245, 249, 1) 0%, rgba(255, 255, 255, 0) 70%);
49
+ top: 50%;
50
+ left: 0;
51
+ transform: translateY(-50%);
52
+ z-index: -1;
53
+ pointer-events: none;
54
+ }
55
+
56
+ .toggle-track {
57
+ width: 36px;
58
+ height: 20px;
59
+ border-radius: 10px;
60
+ background: #cbd5e1;
61
+ position: relative;
62
+ cursor: pointer;
63
+ transition: background 0.2s;
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .toggle-track.active {
68
+ background: #0f172a;
69
+ }
70
+
71
+ .toggle-thumb {
72
+ width: 16px;
73
+ height: 16px;
74
+ border-radius: 50%;
75
+ background: #fff;
76
+ position: absolute;
77
+ top: 2px;
78
+ left: 2px;
79
+ transition: transform 0.2s;
80
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
81
+ }
82
+
83
+ .toggle-track.active .toggle-thumb {
84
+ transform: translateX(16px);
85
+ }
86
+
87
+ .toggle-track.locked {
88
+ opacity: 0.4;
89
+ cursor: not-allowed;
90
+ pointer-events: none;
91
+ }
92
+
93
+ .custom-select {
94
+ appearance: none;
95
+ background: #f8fafc;
96
+ border: 1px solid #e2e8f0;
97
+ border-radius: 8px;
98
+ padding: 6px 32px 6px 12px;
99
+ font-size: 12px;
100
+ font-weight: 600;
101
+ color: #1e293b;
102
+ cursor: pointer;
103
+ outline: none;
104
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
105
+ background-repeat: no-repeat;
106
+ background-position: right 10px center;
107
+ background-size: 14px;
108
+ transition: border-color 0.15s;
109
+ }
110
+
111
+ .custom-select:hover {
112
+ border-color: #94a3b8;
113
+ }
114
+
115
+ .custom-select:focus {
116
+ border-color: #0f172a;
117
+ }
118
+
119
+ .custom-select:disabled {
120
+ opacity: 0.4;
121
+ cursor: not-allowed;
122
+ }
123
+
124
+ .stepper-btn {
125
+ cursor: pointer;
126
+ }
127
+
128
+ .stepper-btn:disabled {
129
+ opacity: 0.3;
130
+ cursor: not-allowed;
131
+ }
132
+
133
+ .setting-card {
134
+ background: #fff;
135
+ border: 1px solid #e2e8f0;
136
+ border-radius: 16px;
137
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
138
+ overflow: hidden;
139
+ }
140
+
141
+ .setting-card-header {
142
+ padding: 16px 24px;
143
+ border-bottom: 1px solid #f1f5f9;
144
+ }
145
+
146
+ .setting-row {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ padding: 14px 24px;
151
+ border-bottom: 1px solid #f8fafc;
152
+ }
153
+
154
+ .setting-row:last-child {
155
+ border-bottom: none;
156
+ }
157
+ </style>
158
+ </head>
159
+
160
+ <body
161
+ class="bg-white text-slate-900 min-h-screen w-full overflow-x-hidden flex flex-col items-center selection:bg-black selection:text-white relative z-0">
162
+
163
+ <div class="bg-glow"></div>
164
+
165
+ <header class="mt-6 flex flex-col items-center flex-shrink-0 w-full z-10">
166
+ <img src="uf_logo.png" alt="UrbanFlow Logo" class="h-28 w-auto object-contain mb-2">
167
+ <div class="flex items-center space-x-3">
168
+ <span class="w-12 h-[1px] bg-slate-200"></span>
169
+ <p class="font-montserrat font-bold tracking-[0.25em] uppercase text-[10px] text-slate-400 text-center">
170
+ Configure & Launch
171
+ </p>
172
+ <span class="w-12 h-[1px] bg-slate-200"></span>
173
+ </div>
174
+ </header>
175
+
176
+ <main class="flex-1 w-full max-w-4xl mx-auto px-8 py-6 z-10 fade-in">
177
+
178
+ <div class="grid grid-cols-2 gap-5 mb-6">
179
+
180
+ <!-- Processing Parameters (locked once started) -->
181
+ <div class="setting-card" id="card-processing">
182
+ <div class="setting-card-header">
183
+ <h3 class="font-montserrat font-bold text-slate-900 text-base">Processing Parameters</h3>
184
+ <p class="text-[10px] font-montserrat font-medium text-slate-400 mt-1 uppercase tracking-widest">
185
+ Auto-configured · adjustable before start</p>
186
+ </div>
187
+
188
+ <!-- Image Size -->
189
+ <div class="setting-row">
190
+ <div>
191
+ <div class="text-xs font-semibold text-slate-700">Image Size</div>
192
+ <div class="text-[10px] text-slate-400 mt-0.5">Model input resolution</div>
193
+ </div>
194
+ <div
195
+ class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
196
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
197
+ onclick="adjust('imgsz',-1)">
198
+ <i class="fa-solid fa-chevron-left text-[10px]"></i>
199
+ </button>
200
+ <span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
201
+ id="val-imgsz">640</span>
202
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
203
+ onclick="adjust('imgsz',1)">
204
+ <i class="fa-solid fa-chevron-right text-[10px]"></i>
205
+ </button>
206
+ </div>
207
+ </div>
208
+
209
+ <!-- Confidence -->
210
+ <div class="setting-row">
211
+ <div>
212
+ <div class="text-xs font-semibold text-slate-700">Confidence</div>
213
+ <div class="text-[10px] text-slate-400 mt-0.5">Minimum detection threshold</div>
214
+ </div>
215
+ <div
216
+ class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
217
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
218
+ onclick="adjust('conf',-1)">
219
+ <i class="fa-solid fa-chevron-left text-[10px]"></i>
220
+ </button>
221
+ <span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
222
+ id="val-conf">0.12</span>
223
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
224
+ onclick="adjust('conf',1)">
225
+ <i class="fa-solid fa-chevron-right text-[10px]"></i>
226
+ </button>
227
+ </div>
228
+ </div>
229
+
230
+ <!-- IoU -->
231
+ <div class="setting-row">
232
+ <div>
233
+ <div class="text-xs font-semibold text-slate-700">IoU Threshold</div>
234
+ <div class="text-[10px] text-slate-400 mt-0.5">Non-max suppression overlap</div>
235
+ </div>
236
+ <div
237
+ class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
238
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
239
+ onclick="adjust('iou',-1)">
240
+ <i class="fa-solid fa-chevron-left text-[10px]"></i>
241
+ </button>
242
+ <span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
243
+ id="val-iou">0.60</span>
244
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
245
+ onclick="adjust('iou',1)">
246
+ <i class="fa-solid fa-chevron-right text-[10px]"></i>
247
+ </button>
248
+ </div>
249
+ </div>
250
+
251
+ <!-- Frame Stride -->
252
+ <div class="setting-row">
253
+ <div>
254
+ <div class="text-xs font-semibold text-slate-700">Frame Stride</div>
255
+ <div class="text-[10px] text-slate-400 mt-0.5">Frames skipped between detections</div>
256
+ </div>
257
+ <div
258
+ class="flex items-center space-x-3 bg-slate-50 px-3 py-1.5 rounded-full border border-slate-200 shadow-sm">
259
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
260
+ onclick="adjust('detect_stride',-1)">
261
+ <i class="fa-solid fa-chevron-left text-[10px]"></i>
262
+ </button>
263
+ <span class="mono-font font-bold w-12 text-center text-slate-900 text-sm"
264
+ id="val-detect_stride">3</span>
265
+ <button class="stepper-btn text-slate-400 hover:text-black transition p-1"
266
+ onclick="adjust('detect_stride',1)">
267
+ <i class="fa-solid fa-chevron-right text-[10px]"></i>
268
+ </button>
269
+ </div>
270
+ </div>
271
+
272
+ <!-- Report Format -->
273
+ <div class="setting-row">
274
+ <div>
275
+ <div class="text-xs font-semibold text-slate-700">Report Format</div>
276
+ <div class="text-[10px] text-slate-400 mt-0.5">Export file type for report charts</div>
277
+ </div>
278
+ <select class="custom-select" id="sel-report-format">
279
+ <option value="png" selected>PNG Image</option>
280
+ <option value="pdf">PDF Document</option>
281
+ </select>
282
+ </div>
283
+
284
+ <!-- Annotated Video -->
285
+ <div class="setting-row">
286
+ <div>
287
+ <div class="text-xs font-semibold text-slate-700">Export Annotated Video</div>
288
+ <div class="text-[10px] text-slate-400 mt-0.5">Bounding boxes + track IDs + counting line</div>
289
+ </div>
290
+ <div class="toggle-track" id="tog-annotated" onclick="toggleSwitch(this)">
291
+ <div class="toggle-thumb"></div>
292
+ </div>
293
+ </div>
294
+ </div>
295
+
296
+ <!-- Display Preferences (editable anytime) -->
297
+ <div class="setting-card" id="card-display">
298
+ <div class="setting-card-header">
299
+ <h3 class="font-montserrat font-bold text-slate-900 text-base">Display Preferences</h3>
300
+ <p class="text-[10px] font-montserrat font-medium text-slate-400 mt-1 uppercase tracking-widest">
301
+ Editable anytime · applies to live charts</p>
302
+ </div>
303
+
304
+ <!-- Chart Color Palette -->
305
+ <div class="setting-row">
306
+ <div>
307
+ <div class="text-xs font-semibold text-slate-700">Chart Color Palette</div>
308
+ <div class="text-[10px] text-slate-400 mt-0.5">Color scheme for all dashboard charts</div>
309
+ </div>
310
+ <select class="custom-select" id="sel-palette">
311
+ <option value="default" selected>Default</option>
312
+ <option value="vibrant">Vibrant</option>
313
+ <option value="corporate">Corporate</option>
314
+ </select>
315
+ </div>
316
+
317
+ <!-- Palette Preview -->
318
+ <div class="px-6 py-4">
319
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-3">Palette Preview
320
+ </div>
321
+ <div class="flex gap-2" id="palette-preview"></div>
322
+ </div>
323
+ </div>
324
+
325
+ </div>
326
+
327
+ <!-- Start Button -->
328
+ <button id="btn-start" onclick="startProcessing()"
329
+ class="w-full py-4 bg-black text-white rounded-full font-montserrat font-semibold hover:bg-slate-800 transition-all text-lg flex justify-center items-center shadow-lg">
330
+ Start Processing <i class="fa-solid fa-arrow-right ml-3 text-sm"></i>
331
+ </button>
332
+
333
+ </main>
334
+
335
+ <script>
336
+ // =========== Config params ===========
337
+ const paramDefs = {
338
+ imgsz: { step: 32, min: 640, max: 1280, decimals: 0 },
339
+ conf: { step: 0.01, min: 0.08, max: 1.0, decimals: 2 },
340
+ iou: { step: 0.05, min: 0.5, max: 1.0, decimals: 2 },
341
+ detect_stride: { step: 1, min: 1, max: 10, decimals: 0 }
342
+ };
343
+
344
+ // =========== Palette definitions ===========
345
+ const PALETTES = {
346
+ default: { congestion: '#f97316', dominance: '#14b8a6', flow: '#3b82f6', label: 'Default' },
347
+ vibrant: { congestion: '#e11d48', dominance: '#8b5cf6', flow: '#06b6d4', label: 'Vibrant' },
348
+ corporate: { congestion: '#1e40af', dominance: '#0d9488', flow: '#475569', label: 'Corporate' }
349
+ };
350
+
351
+ // =========== Load run data from initial.html ===========
352
+ const raw = sessionStorage.getItem('funky_run');
353
+ if (!raw) { window.location.href = '/'; }
354
+ const runData = JSON.parse(raw);
355
+ const config = runData.config;
356
+
357
+ // Populate stepper values from auto-calculated config
358
+ function renderValues() {
359
+ for (const key of Object.keys(paramDefs)) {
360
+ const d = paramDefs[key].decimals;
361
+ const val = config[key];
362
+ document.getElementById('val-' + key).innerText = d ? val.toFixed(d) : val;
363
+ }
364
+ }
365
+ renderValues();
366
+
367
+ // =========== Stepper ===========
368
+ function adjust(key, dir) {
369
+ const p = paramDefs[key];
370
+ let val = config[key] + dir * p.step;
371
+ val = Math.max(p.min, Math.min(p.max, val));
372
+ val = p.decimals ? parseFloat(val.toFixed(p.decimals)) : Math.round(val);
373
+ config[key] = val;
374
+ document.getElementById('val-' + key).innerText = p.decimals ? val.toFixed(p.decimals) : val;
375
+ }
376
+
377
+ // =========== Toggle ===========
378
+ function toggleSwitch(el) {
379
+ if (el.classList.contains('locked')) return;
380
+ el.classList.toggle('active');
381
+ }
382
+
383
+ // =========== Palette preview ===========
384
+ function updatePalettePreview() {
385
+ const key = document.getElementById('sel-palette').value;
386
+ const pal = PALETTES[key];
387
+ const container = document.getElementById('palette-preview');
388
+ const colors = [
389
+ { color: pal.congestion, label: 'Congestion' },
390
+ { color: pal.dominance, label: 'Dominance' },
391
+ { color: pal.flow, label: 'Flow' }
392
+ ];
393
+ container.innerHTML = colors.map(c =>
394
+ `<div class="flex-1 rounded-lg overflow-hidden border border-slate-100">
395
+ <div class="h-8" style="background:${c.color}"></div>
396
+ <div class="text-[9px] font-bold text-slate-500 text-center py-1.5 bg-slate-50">${c.label}</div>
397
+ </div>`
398
+ ).join('');
399
+ }
400
+ document.getElementById('sel-palette').addEventListener('change', updatePalettePreview);
401
+ updatePalettePreview();
402
+
403
+ // =========== Start Processing ===========
404
+ function startProcessing() {
405
+ // Gather settings
406
+ const settings = {
407
+ reportFormat: document.getElementById('sel-report-format').value,
408
+ annotatedVideo: document.getElementById('tog-annotated').classList.contains('active'),
409
+ palette: document.getElementById('sel-palette').value
410
+ };
411
+
412
+ // Update config from steppers (already in config object)
413
+ // Save everything to sessionStorage
414
+ sessionStorage.setItem('funky_run', JSON.stringify({
415
+ video_id: runData.video_id,
416
+ line: runData.line,
417
+ config: config,
418
+ settings: settings
419
+ }));
420
+
421
+ window.location.href = 'vehicles.html';
422
+ }
423
+ </script>
424
+ </body>
425
+
426
+ </html>
frontend/vehicles.html CHANGED
@@ -4,16 +4,18 @@
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>UrbanFlow - Dashboard</title>
8
  <link rel="icon" type="image/svg+xml" href="uf.svg">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
 
 
13
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
14
  <style>
15
  body {
16
- font-family: 'Inter', sans-serif;
17
  }
18
 
19
  .mono-font {
@@ -85,6 +87,126 @@
85
  .info-wrap:hover .info-tip {
86
  display: block;
87
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  </style>
89
  </head>
90
 
@@ -96,35 +218,25 @@
96
  <img src="uf_logo.png" alt="UrbanFlow Logo" class="h-24 w-auto object-contain">
97
  </div>
98
  <nav class="flex-1 px-4 py-4 space-y-1.5 overflow-y-auto text-sm">
 
 
 
 
99
  <a onclick="switchTab('overview')" id="nav-overview"
100
- class="flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer">
101
  <i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
102
  </a>
103
  <a onclick="switchTab('run-details')" id="nav-run-details"
104
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
105
- <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run Details</span>
106
  </a>
107
  <a onclick="switchTab('reports')" id="nav-reports"
108
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
109
  <i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Reports</span>
110
  </a>
111
- <a
112
- class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
113
- <div class="flex items-center"><i class="fa-solid fa-chart-line w-6"></i> <span
114
- class="font-medium">Analytics</span></div>
115
- <i class="fa-solid fa-lock text-[10px]"></i>
116
- </a>
117
- <a
118
- class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
119
- <div class="flex items-center"><i class="fa-solid fa-video w-6"></i> <span class="font-medium">RTSP
120
- Feed</span></div>
121
- <i class="fa-solid fa-lock text-[10px]"></i>
122
- </a>
123
- <a
124
- class="flex items-center justify-between px-4 py-2.5 text-slate-400 bg-slate-50 rounded-lg opacity-60 cursor-not-allowed">
125
- <div class="flex items-center"><i class="fa-solid fa-gear w-6"></i> <span
126
- class="font-medium">Settings</span></div>
127
- <i class="fa-solid fa-lock text-[10px]"></i>
128
  </a>
129
  </nav>
130
  <div class="mt-auto border-t border-slate-100 p-4 flex items-center justify-center bg-white flex-shrink-0">
@@ -134,167 +246,232 @@
134
 
135
  <main class="flex-1 flex flex-col h-full min-w-0 p-4 gap-4">
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  <!-- Progress Bar (shared) -->
138
  <div
139
  class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
140
- <div class="flex items-center space-x-4 w-3/4">
141
- <span class="text-[11px] font-black text-slate-900 uppercase tracking-wider"
142
- id="proc-label">Processing</span>
143
- <div class="flex-1 h-1.5 bg-slate-200 rounded-full overflow-hidden relative">
144
  <div id="proc-bar" class="h-full bg-slate-900 rounded-full transition-all duration-500 ease-out"
145
  style="width: 0%"></div>
146
  </div>
147
  </div>
148
- <div class="flex items-center space-x-6 text-xs font-bold text-slate-900">
149
  <span id="proc-frames">0 / 0 Frames</span>
150
  <span id="proc-pct">0%</span>
151
  </div>
152
  </div>
153
 
154
  <!-- TAB: Overview -->
155
- <div id="tab-overview" class="grid grid-cols-6 gap-4 flex-1 min-h-0 grid-rows-2">
156
 
157
  <!-- Congestion Index -->
158
- <div class="col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
 
159
  <div class="flex justify-between items-center mb-2 relative">
160
- <h3 class="font-bold text-slate-900 text-sm flex items-center">Congestion Index
161
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
162
  <span class="info-tip">Count of active tracked vehicles visible in each processed
163
  frame.</span></span>
164
  </h3>
165
  </div>
166
- <div class="flex-1 relative w-full min-h-0">
167
  <canvas id="congestionChart"></canvas>
168
  </div>
169
  </div>
170
 
171
- <!-- Counters -->
172
- <div class="col-span-1 flex flex-col gap-4">
173
- <div
174
- class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
175
- <div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Total Vehicles
176
- </div>
177
- <div class="flex items-center justify-center mt-1">
 
 
 
 
178
  <span class="text-3xl font-black text-slate-900" id="cnt-total">0</span>
179
- </div>
180
- </div>
181
- <div
182
- class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
183
- <div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Incoming
184
- (Crossed)</div>
185
- <div class="flex items-center justify-center mt-1">
186
- <span class="text-3xl font-black text-slate-900" id="cnt-in">0</span>
187
- </div>
188
- </div>
189
- <div
190
- class="bg-white rounded-xl p-4 border border-slate-200 shadow-sm flex-1 flex flex-col items-center justify-center">
191
- <div class="text-slate-500 text-[10px] font-bold uppercase tracking-wide text-center">Outgoing
192
- (Crossed)</div>
193
- <div class="flex items-center justify-center mt-1">
194
- <span class="text-3xl font-black text-slate-900" id="cnt-out">0</span>
195
  </div>
196
  </div>
197
  </div>
198
 
199
  <!-- Vehicle Classification Breakdown -->
200
  <div
201
- class="col-span-2 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col overflow-hidden">
202
  <div class="flex justify-between items-center mb-1 relative">
203
- <h3 class="font-bold text-slate-900 text-sm flex items-center">Classification
204
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
205
  <span class="info-tip">Per-class vehicle distribution with directional split across the
206
- counting line.</span></span>
207
  </h3>
208
  </div>
209
- <div class="text-[10px] text-slate-400 mb-3">By vehicle subclass</div>
210
- <div class="flex-1 overflow-y-auto pr-3 space-y-2" id="class-breakdown"></div>
211
  </div>
212
 
213
  <!-- Class Dominance -->
214
- <div class="col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
 
215
  <div class="flex justify-between items-center mb-2 relative">
216
- <h3 class="font-bold text-slate-900 text-sm flex items-center">Class Dominance
217
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
218
- <span class="info-tip">Aggregated vehicle count grouped by business category.</span></span>
219
  </h3>
220
- <span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Grouped
221
- Categories</span>
222
  </div>
223
- <div class="flex-1 w-full relative min-h-0">
224
  <canvas id="dominanceChart"></canvas>
225
  </div>
226
  </div>
227
 
228
  <!-- Traffic Flow Over Time -->
229
- <div class="col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col">
 
230
  <div class="flex justify-between items-center mb-2 relative">
231
- <h3 class="font-bold text-slate-900 text-sm flex items-center">Traffic Flow
232
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
233
- <span class="info-tip">Number of vehicles crossing the counting line, binned per second of
234
- video duration.</span></span>
235
  </h3>
236
- <span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Vehicles / Sec</span>
237
  </div>
238
- <div class="flex-1 w-full relative min-h-0">
239
  <canvas id="flowChart"></canvas>
240
  </div>
241
  </div>
242
 
243
  </div>
244
 
245
- <!-- TAB: Run Details -->
246
  <div id="tab-run-details" class="hidden flex-1 min-h-0 overflow-y-auto">
247
- <div class="grid grid-cols-2 gap-4">
248
- <!-- Video Input -->
249
- <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
250
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
251
- <h3 class="font-bold text-slate-800 text-sm"><i
252
- class="fa-solid fa-film mr-2 text-slate-400"></i> Video Input</h3>
253
- </div>
254
- <div class="p-5 space-y-3" id="panel-video"></div>
255
- </div>
256
 
257
- <!-- Performance -->
258
- <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
259
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
260
- <h3 class="font-bold text-slate-800 text-sm"><i
261
- class="fa-solid fa-gauge-high mr-2 text-slate-400"></i> Performance</h3>
 
 
 
 
 
 
 
 
 
262
  </div>
263
- <div class="p-5 space-y-3" id="panel-perf"></div>
264
- </div>
265
-
266
- <!-- Model Config -->
267
- <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
268
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
269
- <h3 class="font-bold text-slate-800 text-sm"><i
270
- class="fa-solid fa-cube mr-2 text-slate-400"></i> Model Config</h3>
271
  </div>
272
- <div class="p-5 space-y-3" id="panel-model"></div>
273
  </div>
274
 
275
- <!-- Inference Settings -->
276
- <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
277
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
278
- <h3 class="font-bold text-slate-800 text-sm"><i
279
- class="fa-solid fa-sliders mr-2 text-slate-400"></i> Inference Settings</h3>
 
 
280
  </div>
281
- <div class="p-5 grid grid-cols-2 gap-x-8 gap-y-3" id="panel-infer"></div>
282
- </div>
283
-
284
- <!-- Processing Results -->
285
- <div
286
- class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
287
- <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
288
- <h3 class="font-bold text-slate-800 text-sm"><i
289
- class="fa-solid fa-stopwatch mr-2 text-slate-400"></i> Processing Results</h3>
290
  </div>
291
- <div class="p-5 space-y-3" id="panel-proc-results">
292
- <div class="text-xs text-slate-400 italic">Waiting for processing to complete...</div>
 
 
 
293
  </div>
294
  </div>
 
295
  </div>
296
  </div>
297
 
 
 
298
  <!-- TAB: Reports -->
299
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
300
  <div id="reports-pending" class="flex flex-col items-center justify-center h-full">
@@ -304,6 +481,122 @@
304
  <div id="reports-grid" class="hidden grid grid-cols-2 xl:grid-cols-3 gap-4"></div>
305
  </div>
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  </main>
308
 
309
  <script>
@@ -326,20 +619,20 @@
326
 
327
  // =========== Tab switching ===========
328
  function switchTab(tab) {
329
- document.getElementById('tab-overview').classList.toggle('hidden', tab !== 'overview');
330
- document.getElementById('tab-run-details').classList.toggle('hidden', tab !== 'run-details');
331
- document.getElementById('tab-reports').classList.toggle('hidden', tab !== 'reports');
332
-
333
- const navO = document.getElementById('nav-overview');
334
- const navR = document.getElementById('nav-run-details');
335
- const navRp = document.getElementById('nav-reports');
336
-
337
- const active = 'flex items-center px-4 py-2.5 bg-slate-900 text-white rounded-lg shadow-md transition cursor-pointer';
338
- const idle = 'flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer';
339
-
340
- navO.className = tab === 'overview' ? active : idle;
341
- navR.className = tab === 'run-details' ? active : idle;
342
- navRp.className = tab === 'reports' ? active : idle;
343
  }
344
 
345
  // =========== Run Details helpers ===========
@@ -397,7 +690,7 @@
397
  document.getElementById('panel-model').innerHTML =
398
  `<div class="flex justify-between items-center border-b border-slate-50 pb-2">
399
  <span class="text-xs font-medium text-slate-500 mono-font">model</span>
400
- <a href="https://huggingface.co/Perception365/VehicleNet-Y26s" target="_blank" class="text-sm font-bold text-blue-600 bg-blue-50 px-2 py-0.5 rounded border border-blue-100 mono-font hover:bg-blue-100 transition">Perception365/VehicleNet-Y26s</a>
401
  </div>` +
402
  detailRow('task', 'detect') +
403
  detailRow('format', 'OpenVINO') +
@@ -405,15 +698,28 @@
405
 
406
  document.getElementById('panel-infer').innerHTML =
407
  infoRow('imgsz', c.imgsz, 'Input image resolution for model inference.') +
408
- infoRow('vid_stride', c.detect_stride, 'Frames skipped between consecutive detections.') +
409
  infoRow('conf', c.conf || 0.12, 'Minimum confidence threshold for valid detections.') +
410
  infoRow('iou', c.iou || 0.60, 'Intersection-over-Union threshold for non-max suppression.') +
411
  infoRow('stream', 'TRUE', 'Frame-by-frame processing for constant memory usage.') +
412
  infoRow('verbose', 'FALSE', 'Console logging suppressed during inference.');
413
  }
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  // =========== Charts ===========
416
- Chart.defaults.font.family = "'Inter', sans-serif";
417
  Chart.defaults.color = '#64748b';
418
 
419
  let MODEL_CLASSES = {};
@@ -421,7 +727,17 @@
421
 
422
  const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
423
  type: 'line',
424
- data: { labels: [], datasets: [{ data: [], borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.08)', fill: true, tension: 0.2, borderWidth: 1.5, pointRadius: 0 }] },
 
 
 
 
 
 
 
 
 
 
425
  options: {
426
  responsive: true, maintainAspectRatio: false,
427
  plugins: { legend: { display: false } },
@@ -433,9 +749,30 @@
433
  }
434
  });
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
437
  type: 'bar',
438
- data: { labels: [], datasets: [{ data: [], backgroundColor: '#14b8a6', borderRadius: 2 }] },
439
  options: {
440
  responsive: true, maintainAspectRatio: false,
441
  plugins: { legend: { display: false } },
@@ -449,7 +786,7 @@
449
 
450
  const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
451
  type: 'bar',
452
- data: { labels: [], datasets: [{ data: [], backgroundColor: '#3b82f6', borderColor: '#ffffff', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
453
  options: {
454
  responsive: true, maintainAspectRatio: false,
455
  plugins: { legend: { display: false } },
@@ -464,11 +801,113 @@
464
  // =========== Update functions ===========
465
  function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
466
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
467
  function updateBreakdown(classIn, classOut) {
468
  const container = document.getElementById('class-breakdown');
469
  const totalAll = sumValues(classIn) + sumValues(classOut);
470
  container.innerHTML = '';
471
 
 
 
 
 
 
 
 
 
472
  Object.keys(MODEL_CLASSES).map(Number).sort((a, b) => a - b).forEach(id => {
473
  const inC = classIn[String(id)] || 0;
474
  const outC = classOut[String(id)] || 0;
@@ -528,30 +967,85 @@
528
  }
529
 
530
  // =========== Main ===========
 
 
531
  async function init() {
532
  const raw = sessionStorage.getItem('funky_run');
533
  if (!raw) { window.location.href = '/'; return; }
534
 
535
- const params = JSON.parse(raw);
 
 
 
536
 
537
  const cRes = await fetch('/constants');
538
  const cData = await cRes.json();
539
  MODEL_CLASSES = cData.classes;
540
  BUSINESS_MAP = cData.business_map;
541
 
 
 
 
 
 
 
 
 
 
 
542
  populateRunDetails(params.config);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
- const videoDuration = params.config.duration || 10;
545
- const stride = params.config.detect_stride || 1;
546
 
547
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
548
  const ws = new WebSocket(`${proto}://${location.host}/ws/run`);
549
 
550
  ws.onopen = () => {
551
  ws.send(JSON.stringify({
552
- video_id: params.video_id,
553
- line: params.line,
554
- config: params.config
 
 
555
  }));
556
  };
557
 
@@ -565,10 +1059,17 @@
565
  document.getElementById('proc-bar').style.width = '100%';
566
  document.getElementById('proc-pct').innerText = '100%';
567
 
568
- document.getElementById('panel-proc-results').innerHTML =
569
- detailRow('processing_time', d.processing_time + ' sec') +
570
- infoRow('actual_fps', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
571
- infoRow('speed_vs_realtime', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
 
 
 
 
 
 
 
572
 
573
  if (d.video_id) loadReports(d.video_id);
574
  return;
@@ -582,8 +1083,10 @@
582
  const totalIn = sumValues(d.class_in);
583
  const totalOut = sumValues(d.class_out);
584
  document.getElementById('cnt-total').innerText = totalIn + totalOut;
585
- document.getElementById('cnt-in').innerText = totalIn;
586
- document.getElementById('cnt-out').innerText = totalOut;
 
 
587
 
588
  const now = performance.now();
589
  if (now - lastUIUpdate < 300) return;
@@ -598,10 +1101,16 @@
598
 
599
  const REPORT_LABELS = {
600
  'direction_pie.png': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
 
601
  'flow_over_time.png': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
 
602
  'congestion_index.png': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
 
603
  'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
 
604
  'confidence_dist.png': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
 
 
605
  };
606
 
607
  async function loadReports(videoId) {
@@ -617,8 +1126,28 @@
617
  data.files.forEach(name => {
618
  const info = REPORT_LABELS[name] || { title: name, desc: '' };
619
  const url = `/reports/${videoId}/${name}`;
 
 
620
  const card = document.createElement('div');
621
  card.className = 'bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  card.innerHTML = `
623
  <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
624
  <div>
@@ -631,7 +1160,7 @@
631
  </a>
632
  </div>
633
  <div class="p-4 flex items-center justify-center bg-slate-50/30">
634
- <img src="${url}" alt="${info.title}" class="max-w-full max-h-[320px] object-contain rounded">
635
  </div>
636
  `;
637
  grid.appendChild(card);
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>UrbanFlow</title>
8
  <link rel="icon" type="image/svg+xml" href="uf.svg">
9
  <script src="https://cdn.tailwindcss.com"></script>
10
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
11
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
12
+ <link
13
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Montserrat:wght@400;500;600;700;800;900&display=swap"
14
+ rel="stylesheet">
15
  <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
16
  <style>
17
  body {
18
+ font-family: 'Montserrat', sans-serif;
19
  }
20
 
21
  .mono-font {
 
87
  .info-wrap:hover .info-tip {
88
  display: block;
89
  }
90
+
91
+ /* Settings controls */
92
+ .toggle-track {
93
+ width: 36px;
94
+ height: 20px;
95
+ border-radius: 10px;
96
+ background: #cbd5e1;
97
+ position: relative;
98
+ cursor: pointer;
99
+ transition: background 0.2s;
100
+ flex-shrink: 0;
101
+ }
102
+
103
+ .toggle-track.active {
104
+ background: #0f172a;
105
+ }
106
+
107
+ .toggle-thumb {
108
+ width: 16px;
109
+ height: 16px;
110
+ border-radius: 50%;
111
+ background: #fff;
112
+ position: absolute;
113
+ top: 2px;
114
+ left: 2px;
115
+ transition: transform 0.2s;
116
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
117
+ }
118
+
119
+ .toggle-track.active .toggle-thumb {
120
+ transform: translateX(16px);
121
+ }
122
+
123
+ .toggle-track.locked {
124
+ opacity: 0.4;
125
+ cursor: not-allowed;
126
+ pointer-events: none;
127
+ }
128
+
129
+ .custom-select {
130
+ appearance: none;
131
+ background: #f8fafc;
132
+ border: 1px solid #e2e8f0;
133
+ border-radius: 8px;
134
+ padding: 6px 32px 6px 12px;
135
+ font-size: 12px;
136
+ font-weight: 600;
137
+ color: #1e293b;
138
+ cursor: pointer;
139
+ outline: none;
140
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2364748b'%3E%3Cpath fill-rule='evenodd' d='M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z'/%3E%3C/svg%3E");
141
+ background-repeat: no-repeat;
142
+ background-position: right 10px center;
143
+ background-size: 14px;
144
+ transition: border-color 0.15s;
145
+ }
146
+
147
+ .custom-select:hover {
148
+ border-color: #94a3b8;
149
+ }
150
+
151
+ .custom-select:disabled {
152
+ opacity: 0.4;
153
+ cursor: not-allowed;
154
+ }
155
+
156
+ .s-stepper {
157
+ display: inline-flex;
158
+ align-items: center;
159
+ border: 1px solid #e2e8f0;
160
+ border-radius: 10px;
161
+ background: #f8fafc;
162
+ overflow: hidden;
163
+ }
164
+
165
+ .s-stepper button {
166
+ background: none;
167
+ border: none;
168
+ padding: 4px 10px;
169
+ cursor: pointer;
170
+ color: #64748b;
171
+ font-size: 14px;
172
+ line-height: 1;
173
+ }
174
+
175
+ .s-stepper button:hover {
176
+ background: #e2e8f0;
177
+ }
178
+
179
+ .s-stepper button:disabled {
180
+ opacity: 0.25;
181
+ cursor: not-allowed;
182
+ }
183
+
184
+ .s-stepper .s-val {
185
+ min-width: 48px;
186
+ text-align: center;
187
+ font-family: 'JetBrains Mono', monospace;
188
+ font-size: 13px;
189
+ font-weight: 700;
190
+ color: #0f172a;
191
+ padding: 4px 0;
192
+ }
193
+
194
+ .s-row {
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: space-between;
198
+ padding: 10px 0;
199
+ border-bottom: 1px solid #f1f5f9;
200
+ }
201
+
202
+ .s-row:last-child {
203
+ border-bottom: none;
204
+ }
205
+
206
+ .s-row.disabled {
207
+ opacity: 0.45;
208
+ pointer-events: none;
209
+ }
210
  </style>
211
  </head>
212
 
 
218
  <img src="uf_logo.png" alt="UrbanFlow Logo" class="h-24 w-auto object-contain">
219
  </div>
220
  <nav class="flex-1 px-4 py-4 space-y-1.5 overflow-y-auto text-sm">
221
+ <a onclick="switchTab('about')" id="nav-about"
222
+ class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
223
+ <i class="fa-solid fa-circle-info w-6"></i> <span class="font-medium">About</span>
224
+ </a>
225
  <a onclick="switchTab('overview')" id="nav-overview"
226
+ class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
227
  <i class="fa-solid fa-desktop w-6"></i> <span class="font-medium">Overview</span>
228
  </a>
229
  <a onclick="switchTab('run-details')" id="nav-run-details"
230
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
231
+ <i class="fa-solid fa-microchip w-6"></i> <span class="font-medium">Run</span>
232
  </a>
233
  <a onclick="switchTab('reports')" id="nav-reports"
234
  class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
235
  <i class="fa-solid fa-file-lines w-6"></i> <span class="font-medium">Reports</span>
236
  </a>
237
+ <a onclick="switchTab('settings')" id="nav-settings"
238
+ class="flex items-center px-4 py-2.5 text-slate-600 hover:bg-slate-50 hover:text-slate-900 rounded-lg transition cursor-pointer">
239
+ <i class="fa-solid fa-sliders w-6"></i> <span class="font-medium">Settings</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
240
  </a>
241
  </nav>
242
  <div class="mt-auto border-t border-slate-100 p-4 flex items-center justify-center bg-white flex-shrink-0">
 
246
 
247
  <main class="flex-1 flex flex-col h-full min-w-0 p-4 gap-4">
248
 
249
+ <!-- TAB: About -->
250
+ <div id="tab-about" class="hidden flex-1 min-h-0 overflow-y-auto">
251
+ <div class="bg-white rounded-xl border border-slate-200 shadow-sm p-8 max-w-[1400px] w-full">
252
+ <h2 class="text-2xl font-bold text-slate-800 mb-6">UrbanFlow</h2>
253
+
254
+ <div class="mb-10 py-6 border-y border-slate-50">
255
+ <p
256
+ class="max-w-3xl mx-auto text-center text-slate-400 text-sm font-semibold leading-relaxed tracking-wide italic px-8">
257
+ "Urban Flow is more than a metric of transit; it is the pulse of a city’s collective intent.
258
+ This workspace is designed to observe that rhythm, transforming raw motion into the clarity
259
+ required for decisive, visionary infrastructure."
260
+ </p>
261
+ </div>
262
+
263
+ <div class="space-y-6 text-slate-600 leading-relaxed">
264
+ <p>
265
+ UrbanFlow is an advanced vision intelligence suite designed for real-time traffic dynamics
266
+ analysis.
267
+ Leveraging state-of-the-art deep learning models, it provides consultative-grade insights into
268
+ vehicle movement, classification, and throughput.
269
+ </p>
270
+ <p>
271
+ This platform is engineered for urban planners and infrastructure executives who require
272
+ high-density data to drive visionary urban transformation. By transforming raw video footage
273
+ into actionable intelligence, UrbanFlow enables data-driven decision making for safer,
274
+ more efficient urban mobility.
275
+ </p>
276
+ <div class="grid grid-cols-2 gap-8 pt-6 border-t border-slate-50">
277
+ <div class="space-y-4">
278
+ <h4 class="font-bold text-slate-800 text-sm uppercase tracking-wider">Core Capabilities</h4>
279
+ <ul class="text-xs space-y-2">
280
+ <li class="flex items-center gap-2"><i
281
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> Real-time Spatial
282
+ Detection</li>
283
+ <li class="flex items-center gap-2"><i
284
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> Multi-class Object
285
+ Tracking</li>
286
+ <li class="flex items-center gap-2"><i
287
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> Bidirectional Flow
288
+ Analysis</li>
289
+ <li class="flex items-center gap-2"><i
290
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> High-Density Metrics
291
+ Capture</li>
292
+ </ul>
293
+ </div>
294
+ <div class="space-y-4">
295
+ <h4 class="font-bold text-slate-800 text-sm uppercase tracking-wider">Intelligence Framework
296
+ </h4>
297
+ <ul class="text-xs space-y-2">
298
+ <li class="flex items-center gap-2"><i
299
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> OpenVINO Performance
300
+ Optimization</li>
301
+ <li class="flex items-center gap-2"><i
302
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> ByteTrack Temporal
303
+ Assignment</li>
304
+ <li class="flex items-center gap-2"><i
305
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> Sub-millimeter
306
+ Coordinate Logic</li>
307
+ <li class="flex items-center gap-2"><i
308
+ class="fa-solid fa-circle text-[6px] text-slate-400"></i> Enterprise MIS
309
+ Compliance</li>
310
+ </ul>
311
+ </div>
312
+ </div>
313
+ </div>
314
+ </div>
315
+ </div>
316
+
317
+
318
  <!-- Progress Bar (shared) -->
319
  <div
320
  class="bg-white rounded-xl px-6 py-4 border border-slate-200 shadow-sm flex items-center justify-between flex-shrink-0">
321
+ <div class="flex items-center space-x-4 flex-1 mr-6">
322
+ <span class="text-[11px] font-black text-slate-900 uppercase tracking-wider whitespace-nowrap"
323
+ id="proc-label">Waiting</span>
324
+ <div class="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden relative">
325
  <div id="proc-bar" class="h-full bg-slate-900 rounded-full transition-all duration-500 ease-out"
326
  style="width: 0%"></div>
327
  </div>
328
  </div>
329
+ <div class="flex items-center space-x-6 text-xs font-bold text-slate-900 whitespace-nowrap">
330
  <span id="proc-frames">0 / 0 Frames</span>
331
  <span id="proc-pct">0%</span>
332
  </div>
333
  </div>
334
 
335
  <!-- TAB: Overview -->
336
+ <div id="tab-overview" class="grid grid-cols-12 gap-4 flex-1 min-h-0 overflow-hidden">
337
 
338
  <!-- Congestion Index -->
339
+ <div
340
+ class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
341
  <div class="flex justify-between items-center mb-2 relative">
342
+ <h3 class="font-bold text-slate-900 text-sm flex items-center">Real-time Congestion Index
343
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
344
  <span class="info-tip">Count of active tracked vehicles visible in each processed
345
  frame.</span></span>
346
  </h3>
347
  </div>
348
+ <div class="flex-1 relative w-full min-h-[220px]">
349
  <canvas id="congestionChart"></canvas>
350
  </div>
351
  </div>
352
 
353
+ <!-- Doughnut — In / Out -->
354
+ <div
355
+ class="col-span-6 xl:col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col items-center justify-center min-h-0">
356
+ <h3 class="font-bold text-slate-900 text-sm mb-2 self-start flex items-center">Directional Distribution
357
+ <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
358
+ <span class="info-tip">Incoming vs outgoing vehicles analyzed within the spatial
359
+ boundary.</span></span>
360
+ </h3>
361
+ <div class="relative w-full flex-1 min-h-[180px] flex items-center justify-center">
362
+ <canvas id="doughnutChart"></canvas>
363
+ <div class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
364
  <span class="text-3xl font-black text-slate-900" id="cnt-total">0</span>
365
+ <span class="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Total Units</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  </div>
367
  </div>
368
  </div>
369
 
370
  <!-- Vehicle Classification Breakdown -->
371
  <div
372
+ class="col-span-6 xl:col-span-3 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col overflow-hidden min-h-0">
373
  <div class="flex justify-between items-center mb-1 relative">
374
+ <h3 class="font-bold text-slate-900 text-sm flex items-center">Vehicle Classification
375
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
376
  <span class="info-tip">Per-class vehicle distribution with directional split across the
377
+ spatial boundary.</span></span>
378
  </h3>
379
  </div>
380
+ <div class="text-[10px] text-slate-400 mb-3 uppercase tracking-widest">Inventory Breakdown</div>
381
+ <div class="flex-1 overflow-y-auto pr-2 space-y-2" id="class-breakdown"></div>
382
  </div>
383
 
384
  <!-- Class Dominance -->
385
+ <div
386
+ class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
387
  <div class="flex justify-between items-center mb-2 relative">
388
+ <h3 class="font-bold text-slate-900 text-sm flex items-center">Category Dominance
389
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
390
+ <span class="info-tip">Aggregated vehicle count grouped by dominant category.</span></span>
391
  </h3>
392
+ <span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Dominant
393
+ Intelligence</span>
394
  </div>
395
+ <div class="flex-1 w-full relative min-h-[220px]">
396
  <canvas id="dominanceChart"></canvas>
397
  </div>
398
  </div>
399
 
400
  <!-- Traffic Flow Over Time -->
401
+ <div
402
+ class="col-span-12 xl:col-span-6 bg-white rounded-xl p-5 border border-slate-200 shadow-sm flex flex-col min-h-0">
403
  <div class="flex justify-between items-center mb-2 relative">
404
+ <h3 class="font-bold text-slate-900 text-sm flex items-center">Temporal Traffic Flow
405
  <span class="info-wrap"><span class="info-btn"><i class="fa-solid fa-info"></i></span>
406
+ <span class="info-tip">Number of vehicles analyzed within the spatial boundary, binned per
407
+ second of video duration.</span></span>
408
  </h3>
409
+ <span class="text-[10px] text-slate-500 font-bold uppercase tracking-wider">Units / Second</span>
410
  </div>
411
+ <div class="flex-1 w-full relative min-h-[220px]">
412
  <canvas id="flowChart"></canvas>
413
  </div>
414
  </div>
415
 
416
  </div>
417
 
418
+ <!-- TAB: Run -->
419
  <div id="tab-run-details" class="hidden flex-1 min-h-0 overflow-y-auto">
420
+ <div class="space-y-6 w-full max-w-[1400px]">
 
 
 
 
 
 
 
 
421
 
422
+ <!-- HERO: Process Analytics -->
423
+ <div id="run-results-card"
424
+ class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
425
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
426
+ <div>
427
+ <h3 class="font-bold text-slate-800 text-sm">Process Analytics</h3>
428
+ <p
429
+ class="text-[10px] text-slate-400 mt-0.5 uppercase tracking-widest font-medium text-center">
430
+ Execution Telemetry</p>
431
+ </div>
432
+ <div id="results-status-badge"
433
+ class="px-2.5 py-1 bg-slate-100 text-slate-500 text-[10px] font-bold rounded-full uppercase tracking-tighter">
434
+ Awaiting Execution
435
+ </div>
436
  </div>
437
+ <div class="p-8">
438
+ <div id="run-results-content" class="grid grid-cols-3 gap-12">
439
+ <div
440
+ class="flex flex-col items-center justify-center p-8 border-2 border-dashed border-slate-100 rounded-2xl col-span-3 text-slate-300">
441
+ <i class="fa-solid fa-chart-line text-4xl mb-4"></i>
442
+ <span class="text-xs font-semibold">Initiate a run to view performance insights</span>
443
+ </div>
444
+ </div>
445
  </div>
 
446
  </div>
447
 
448
+ <!-- Technical Context Row -->
449
+ <div class="grid grid-cols-3 gap-6">
450
+ <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
451
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
452
+ <h3 class="font-bold text-slate-800 text-sm">Stream Source Profile</h3>
453
+ </div>
454
+ <div class="p-6 space-y-4" id="panel-video"></div>
455
  </div>
456
+ <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
457
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
458
+ <h3 class="font-bold text-slate-800 text-sm">System Resource Utilization</h3>
459
+ </div>
460
+ <div class="p-6 space-y-4" id="panel-perf"></div>
 
 
 
 
461
  </div>
462
+ <div class="bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden">
463
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
464
+ <h3 class="font-bold text-slate-800 text-sm">Model Architecture & Logic</h3>
465
+ </div>
466
+ <div class="p-6 space-y-4" id="panel-model"></div>
467
  </div>
468
  </div>
469
+
470
  </div>
471
  </div>
472
 
473
+
474
+
475
  <!-- TAB: Reports -->
476
  <div id="tab-reports" class="hidden flex-1 min-h-0 overflow-y-auto">
477
  <div id="reports-pending" class="flex flex-col items-center justify-center h-full">
 
481
  <div id="reports-grid" class="hidden grid grid-cols-2 xl:grid-cols-3 gap-4"></div>
482
  </div>
483
 
484
+ <!-- TAB: Settings -->
485
+ <div id="tab-settings" class="hidden flex-1 min-h-0 overflow-y-auto">
486
+ <div class="grid grid-cols-3 gap-6 w-full">
487
+
488
+ <!-- Processing Parameters -->
489
+ <div class="col-span-2 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
490
+ <div class="px-6 py-4 border-b border-slate-100 bg-slate-50/50">
491
+ <h3 class="font-bold text-slate-800 text-sm flex items-center">Inference Configuration Profile
492
+ <span class="info-wrap">
493
+ <span class="info-btn"><i class="fa-solid fa-info"></i></span>
494
+ <span class="info-tip">Figures and numbers below are auto-calculated for best
495
+ performance.</span>
496
+ </span>
497
+ </h3>
498
+ <p class="text-[10px] text-slate-400 mt-0.1 uppercase tracking-widest font-medium">
499
+ Auto-configured pipeline parameters</p>
500
+ </div>
501
+ <div class="px-6 py-2" id="settings-params">
502
+ <div class="s-row" data-param="imgsz">
503
+ <div>
504
+ <div class="text-xs font-semibold text-slate-700">Image Size</div>
505
+ <div class="text-[10px] text-slate-400">Model input resolution</div>
506
+ </div>
507
+ <div class="s-stepper"><button onclick="stepParam('imgsz',-32)">&#8249;</button><span
508
+ class="s-val" id="sv-imgsz">640</span><button
509
+ onclick="stepParam('imgsz',32)">&#8250;</button></div>
510
+ </div>
511
+ <div class="s-row" data-param="conf">
512
+ <div>
513
+ <div class="text-xs font-semibold text-slate-700">Confidence</div>
514
+ <div class="text-[10px] text-slate-400">Minimum detection threshold</div>
515
+ </div>
516
+ <div class="s-stepper"><button onclick="stepParam('conf',-0.01)">&#8249;</button><span
517
+ class="s-val" id="sv-conf">0.12</span><button
518
+ onclick="stepParam('conf',0.01)">&#8250;</button></div>
519
+ </div>
520
+ <div class="s-row" data-param="iou">
521
+ <div>
522
+ <div class="text-xs font-semibold text-slate-700">IoU Threshold</div>
523
+ <div class="text-[10px] text-slate-400">Non-max suppression overlap</div>
524
+ </div>
525
+ <div class="s-stepper"><button onclick="stepParam('iou',-0.05)">&#8249;</button><span
526
+ class="s-val" id="sv-iou">0.60</span><button
527
+ onclick="stepParam('iou',0.05)">&#8250;</button></div>
528
+ </div>
529
+ <div class="s-row" data-param="stride">
530
+ <div>
531
+ <div class="text-xs font-semibold text-slate-700">Frame Stride</div>
532
+ <div class="text-[10px] text-slate-400">Frames skipped between detections</div>
533
+ </div>
534
+ <div class="s-stepper"><button onclick="stepParam('stride',-1)">&#8249;</button><span
535
+ class="s-val" id="sv-stride">2</span><button
536
+ onclick="stepParam('stride',1)">&#8250;</button></div>
537
+ </div>
538
+ <div class="s-row" data-param="report">
539
+ <div>
540
+ <div class="text-xs font-semibold text-slate-700">Report Format</div>
541
+ <div class="text-[10px] text-slate-400">Export file type for charts</div>
542
+ </div>
543
+ <select class="custom-select" id="sv-report">
544
+ <option value="png" selected>PNG Image</option>
545
+ <option value="pdf">PDF Document</option>
546
+ </select>
547
+ </div>
548
+ <div class="s-row" data-param="annotated">
549
+ <div>
550
+ <div class="text-xs font-semibold text-slate-700">Export Annotated Video</div>
551
+ <div class="text-[10px] text-slate-400">Bounding boxes + track IDs + spatial boundary
552
+ overlays</div>
553
+ </div>
554
+ <div class="toggle-track" id="sv-annotated" onclick="this.classList.toggle('active')">
555
+ <div class="toggle-thumb"></div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ </div>
560
+
561
+ <!-- Display Preferences -->
562
+ <div
563
+ class="col-span-1 bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col">
564
+ <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50">
565
+ <h3 class="font-bold text-slate-800 text-sm">Display Preferences</h3>
566
+ </div>
567
+ <div class="px-5 py-4 flex-1 flex flex-col">
568
+ <div class="s-row">
569
+ <div>
570
+ <div class="text-xs font-semibold text-slate-700">Chart Color Palette</div>
571
+ <div class="text-[10px] text-slate-400">Color scheme for all charts</div>
572
+ </div>
573
+ <select class="custom-select" id="live-palette-select" onchange="applyPalette(this.value)">
574
+ <option value="default">Default</option>
575
+ <option value="vibrant">Vibrant</option>
576
+ <option value="corporate">Corporate</option>
577
+ </select>
578
+ </div>
579
+ <div class="mt-3">
580
+ <div class="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-2">Palette
581
+ Preview</div>
582
+ <div class="flex gap-1.5" id="live-palette-preview"></div>
583
+ </div>
584
+ </div>
585
+ </div>
586
+
587
+ <!-- Start Button -->
588
+ <div class="col-span-3 pb-4" id="settings-start-wrap">
589
+ <button id="btn-start-processing" onclick="startProcessingFromSettings()"
590
+ class="w-full py-4 bg-slate-900 text-white font-medium text-sm rounded-full hover:bg-slate-800 active:scale-[0.99] transition flex items-center justify-center gap-2 shadow-lg">
591
+ <span>Process &nbsp;&rarr;</span>
592
+ </button>
593
+ </div>
594
+
595
+
596
+
597
+ </div>
598
+ </div>
599
+
600
  </main>
601
 
602
  <script>
 
619
 
620
  // =========== Tab switching ===========
621
  function switchTab(tab) {
622
+ ['about', 'overview', 'run-details', 'reports', 'settings'].forEach(t => {
623
+ const el = document.getElementById('tab-' + t);
624
+ const nav = document.getElementById('nav-' + t);
625
+ if (el) el.classList.toggle('hidden', tab !== t);
626
+ if (nav) {
627
+ if (tab === t) {
628
+ nav.classList.add('bg-slate-900', 'text-white', 'shadow-md');
629
+ nav.classList.remove('text-slate-600', 'hover:bg-slate-50', 'hover:text-slate-900');
630
+ } else {
631
+ nav.classList.remove('bg-slate-900', 'text-white', 'shadow-md');
632
+ nav.classList.add('text-slate-600', 'hover:bg-slate-50', 'hover:text-slate-900');
633
+ }
634
+ }
635
+ });
636
  }
637
 
638
  // =========== Run Details helpers ===========
 
690
  document.getElementById('panel-model').innerHTML =
691
  `<div class="flex justify-between items-center border-b border-slate-50 pb-2">
692
  <span class="text-xs font-medium text-slate-500 mono-font">model</span>
693
+ <a href="https://huggingface.co/Perception365/VehicleNet-Y26s" target="_blank" class="text-sm font-bold text-slate-800 mono-font hover:text-slate-600 transition underline underline-offset-4 decoration-slate-200">Perception365/VehicleNet-Y26s</a>
694
  </div>` +
695
  detailRow('task', 'detect') +
696
  detailRow('format', 'OpenVINO') +
 
698
 
699
  document.getElementById('panel-infer').innerHTML =
700
  infoRow('imgsz', c.imgsz, 'Input image resolution for model inference.') +
701
+ infoRow('vid_stride', c.detect_stride, 'Frames skipped between consecutive detections relative to the spatial boundary.') +
702
  infoRow('conf', c.conf || 0.12, 'Minimum confidence threshold for valid detections.') +
703
  infoRow('iou', c.iou || 0.60, 'Intersection-over-Union threshold for non-max suppression.') +
704
  infoRow('stream', 'TRUE', 'Frame-by-frame processing for constant memory usage.') +
705
  infoRow('verbose', 'FALSE', 'Console logging suppressed during inference.');
706
  }
707
 
708
+ // =========== Palettes ===========
709
+ const PALETTES = {
710
+ default: { congestion: '#f97316', congestionBg: 'rgba(249,115,22,0.08)', dominance: '#14b8a6', flow: '#3b82f6', doughIn: '#3b82f6', doughOut: '#f97316' },
711
+ vibrant: { congestion: '#e11d48', congestionBg: 'rgba(225,29,72,0.08)', dominance: '#8b5cf6', flow: '#06b6d4', doughIn: '#06b6d4', doughOut: '#e11d48' },
712
+ corporate: { congestion: '#1e40af', congestionBg: 'rgba(30,64,175,0.08)', dominance: '#0d9488', flow: '#475569', doughIn: '#475569', doughOut: '#1e40af' }
713
+ };
714
+
715
+ // Read settings from sessionStorage (set by settings.html)
716
+ const rawRun = sessionStorage.getItem('funky_run');
717
+ const runSettings = rawRun ? (JSON.parse(rawRun).settings || {}) : {};
718
+ let currentPalette = runSettings.palette || 'default';
719
+ let activePalette = PALETTES[currentPalette];
720
+
721
  // =========== Charts ===========
722
+ Chart.defaults.font.family = "'Montserrat', sans-serif";
723
  Chart.defaults.color = '#64748b';
724
 
725
  let MODEL_CLASSES = {};
 
727
 
728
  const congChart = new Chart(document.getElementById('congestionChart').getContext('2d'), {
729
  type: 'line',
730
+ data: {
731
+ labels: [], datasets: [{
732
+ data: [],
733
+ borderColor: activePalette.congestion,
734
+ backgroundColor: activePalette.congestionBg,
735
+ fill: true,
736
+ tension: 0.2,
737
+ borderWidth: 1.5,
738
+ pointRadius: 0
739
+ }]
740
+ },
741
  options: {
742
  responsive: true, maintainAspectRatio: false,
743
  plugins: { legend: { display: false } },
 
749
  }
750
  });
751
 
752
+ const doughChart = new Chart(document.getElementById('doughnutChart').getContext('2d'), {
753
+ type: 'doughnut',
754
+ data: {
755
+ labels: ['Incoming', 'Outgoing'], datasets: [{
756
+ data: [0, 0],
757
+ backgroundColor: [activePalette.doughIn, activePalette.doughOut],
758
+ borderColor: '#ffffff',
759
+ borderWidth: 3,
760
+ hoverOffset: 6
761
+ }]
762
+ },
763
+ options: {
764
+ responsive: true, maintainAspectRatio: false,
765
+ cutout: '68%',
766
+ plugins: {
767
+ legend: { display: true, position: 'bottom', labels: { padding: 12, usePointStyle: true, pointStyle: 'circle', font: { size: 10, weight: '600' } } }
768
+ },
769
+ animation: { duration: 0 }
770
+ }
771
+ });
772
+
773
  const domChart = new Chart(document.getElementById('dominanceChart').getContext('2d'), {
774
  type: 'bar',
775
+ data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.dominance, borderRadius: 2 }] },
776
  options: {
777
  responsive: true, maintainAspectRatio: false,
778
  plugins: { legend: { display: false } },
 
786
 
787
  const flowChart = new Chart(document.getElementById('flowChart').getContext('2d'), {
788
  type: 'bar',
789
+ data: { labels: [], datasets: [{ data: [], backgroundColor: activePalette.flow, borderColor: '#ffffff', borderWidth: 1.5, barPercentage: 1.0, categoryPercentage: 1.0 }] },
790
  options: {
791
  responsive: true, maintainAspectRatio: false,
792
  plugins: { legend: { display: false } },
 
801
  // =========== Update functions ===========
802
  function sumValues(obj) { return Object.values(obj).reduce((a, b) => a + b, 0); }
803
 
804
+ // =========== Live Palette Switching ===========
805
+ function applyPalette(key) {
806
+ activePalette = PALETTES[key] || PALETTES.default;
807
+ currentPalette = key;
808
+
809
+ // Congestion
810
+ congChart.data.datasets[0].borderColor = activePalette.congestion;
811
+ congChart.data.datasets[0].backgroundColor = activePalette.congestionBg;
812
+ congChart.update();
813
+
814
+ // Doughnut
815
+ doughChart.data.datasets[0].backgroundColor = [activePalette.doughIn, activePalette.doughOut];
816
+ doughChart.update();
817
+
818
+ // Dominance
819
+ domChart.data.datasets[0].backgroundColor = activePalette.dominance;
820
+ domChart.update();
821
+
822
+ // Flow
823
+ flowChart.data.datasets[0].backgroundColor = activePalette.flow;
824
+ flowChart.update();
825
+
826
+ // Update palette preview
827
+ renderPalettePreview(key);
828
+ }
829
+
830
+ function renderPalettePreview(key) {
831
+ const pal = PALETTES[key];
832
+ const colors = [
833
+ { color: pal.congestion, label: 'Congestion' },
834
+ { color: pal.dominance, label: 'Dominance' },
835
+ { color: pal.flow, label: 'Flow' },
836
+ { color: pal.doughIn, label: 'Incoming' },
837
+ { color: pal.doughOut, label: 'Outgoing' }
838
+ ];
839
+ const el = document.getElementById('live-palette-preview');
840
+ if (el) {
841
+ el.innerHTML = colors.map(c =>
842
+ `<div class="flex-1 rounded-lg overflow-hidden border border-slate-100">
843
+ <div class="h-6" style="background:${c.color}"></div>
844
+ <div class="text-[8px] font-bold text-slate-400 text-center py-1 bg-slate-50">${c.label}</div>
845
+ </div>`
846
+ ).join('');
847
+ }
848
+ }
849
+
850
+ function populateSettingsTab(config, settings) {
851
+ // Populate stepper values from config
852
+ document.getElementById('sv-imgsz').textContent = config.imgsz || 640;
853
+ document.getElementById('sv-conf').textContent = (config.conf || 0.12).toFixed(2);
854
+ document.getElementById('sv-iou').textContent = (config.iou || 0.60).toFixed(2);
855
+ document.getElementById('sv-stride').textContent = config.detect_stride || 2;
856
+
857
+ // Populate export settings
858
+ const selReport = document.getElementById('sv-report');
859
+ if (selReport) selReport.value = settings.reportFormat || 'png';
860
+ const togAnnot = document.getElementById('sv-annotated');
861
+ if (togAnnot && settings.annotatedVideo) togAnnot.classList.add('active');
862
+
863
+ // Set live palette dropdown
864
+ const sel = document.getElementById('live-palette-select');
865
+ if (sel) sel.value = currentPalette;
866
+ renderPalettePreview(currentPalette);
867
+ }
868
+
869
+ // =========== Settings Stepper Logic ===========
870
+ const PARAM_LIMITS = {
871
+ imgsz: { min: 640, max: 1280 },
872
+ conf: { min: 0.10, max: 0.95 },
873
+ iou: { min: 0.50, max: 0.95 },
874
+ stride: { min: 1, max: 10 }
875
+ };
876
+
877
+ function stepParam(param, delta) {
878
+ const el = document.getElementById('sv-' + param);
879
+ if (!el) return;
880
+ const limits = PARAM_LIMITS[param];
881
+ let val = parseFloat(el.textContent);
882
+ val = Math.round((val + delta) * 100) / 100;
883
+ val = Math.max(limits.min, Math.min(limits.max, val));
884
+ el.textContent = (param === 'conf' || param === 'iou') ? val.toFixed(2) : val;
885
+ }
886
+
887
+ function lockSettings() {
888
+ // Disable critical param rows
889
+ document.querySelectorAll('#settings-params .s-row').forEach(row => {
890
+ const p = row.dataset.param;
891
+ if (p && p !== 'palette') row.classList.add('disabled');
892
+ });
893
+ // Hide start button
894
+ const wrap = document.getElementById('settings-start-wrap');
895
+ if (wrap) wrap.style.display = 'none';
896
+ }
897
+
898
  function updateBreakdown(classIn, classOut) {
899
  const container = document.getElementById('class-breakdown');
900
  const totalAll = sumValues(classIn) + sumValues(classOut);
901
  container.innerHTML = '';
902
 
903
+ // Update Doughnut border logic (remove gap if unidirectional)
904
+ const sumIn = sumValues(classIn);
905
+ const sumOut = sumValues(classOut);
906
+ doughChart.data.datasets[0].borderWidth = (sumIn === 0 || sumOut === 0) ? 0 : 3;
907
+ doughChart.data.datasets[0].data = [sumIn, sumOut];
908
+ doughChart.update();
909
+ document.getElementById('cnt-total').innerText = totalAll;
910
+
911
  Object.keys(MODEL_CLASSES).map(Number).sort((a, b) => a - b).forEach(id => {
912
  const inC = classIn[String(id)] || 0;
913
  const outC = classOut[String(id)] || 0;
 
967
  }
968
 
969
  // =========== Main ===========
970
+ let _params = null;
971
+
972
  async function init() {
973
  const raw = sessionStorage.getItem('funky_run');
974
  if (!raw) { window.location.href = '/'; return; }
975
 
976
+ _params = JSON.parse(raw);
977
+
978
+ // SECURITY: Clear session storage so refresh always redirects home
979
+ sessionStorage.removeItem('funky_run');
980
 
981
  const cRes = await fetch('/constants');
982
  const cData = await cRes.json();
983
  MODEL_CLASSES = cData.classes;
984
  BUSINESS_MAP = cData.business_map;
985
 
986
+ populateAndInit(_params);
987
+
988
+ // SECURITY: Clear session storage after populate so refresh triggers redirect
989
+ sessionStorage.removeItem('funky_run');
990
+
991
+ // Show Settings tab first, but also initialized with About implicitly in sidebar hierarchy
992
+ switchTab('settings');
993
+ }
994
+
995
+ function populateAndInit(params) {
996
  populateRunDetails(params.config);
997
+ populateSettingsTab(params.config, params.settings || {});
998
+ }
999
+
1000
+ function startProcessingFromSettings() {
1001
+ if (!_params) return;
1002
+
1003
+ // Read current stepper/control values
1004
+ const imgsz = parseInt(document.getElementById('sv-imgsz').textContent);
1005
+ const conf = parseFloat(document.getElementById('sv-conf').textContent);
1006
+ const iou = parseFloat(document.getElementById('sv-iou').textContent);
1007
+ const stride = parseInt(document.getElementById('sv-stride').textContent);
1008
+ const reportFmt = document.getElementById('sv-report').value;
1009
+ const annotated = document.getElementById('sv-annotated').classList.contains('active');
1010
+
1011
+ // Apply to config
1012
+ _params.config.imgsz = imgsz;
1013
+ _params.config.conf = conf;
1014
+ _params.config.iou = iou;
1015
+ _params.config.detect_stride = stride;
1016
+
1017
+ // Lock settings
1018
+ lockSettings();
1019
+
1020
+ // Switch to overview
1021
+ switchTab('overview');
1022
+ document.getElementById('proc-label').innerText = 'Processing';
1023
+
1024
+ // Reset Run Tab Results to Awaiting
1025
+ const badge = document.getElementById('results-status-badge');
1026
+ if (badge) {
1027
+ badge.innerText = 'Processing';
1028
+ badge.className = 'px-2.5 py-1 bg-blue-100 text-blue-700 text-[10px] font-bold rounded-full uppercase tracking-tighter animate-pulse';
1029
+ }
1030
+ document.getElementById('run-results-content').innerHTML = `
1031
+ <div class="flex flex-col items-center justify-center p-8 bg-slate-50/50 border border-slate-100 rounded-2xl col-span-3 text-slate-400">
1032
+ <i class="fa-solid fa-spinner fa-spin text-2xl mb-3"></i>
1033
+ <span class="text-xs font-semibold">Executing inference pipeline... results pending</span>
1034
+ </div>`;
1035
 
1036
+ // Start WebSocket
1037
+ const videoDuration = _params.config.duration || 10;
1038
 
1039
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
1040
  const ws = new WebSocket(`${proto}://${location.host}/ws/run`);
1041
 
1042
  ws.onopen = () => {
1043
  ws.send(JSON.stringify({
1044
+ video_id: _params.video_id,
1045
+ line: _params.line,
1046
+ config: _params.config,
1047
+ annotated_video: annotated,
1048
+ report_format: reportFmt
1049
  }));
1050
  };
1051
 
 
1059
  document.getElementById('proc-bar').style.width = '100%';
1060
  document.getElementById('proc-pct').innerText = '100%';
1061
 
1062
+ // Update Run Tab Badge
1063
+ const badge = document.getElementById('results-status-badge');
1064
+ if (badge) {
1065
+ badge.innerText = 'Completed';
1066
+ badge.className = 'px-2.5 py-1 bg-green-100 text-green-700 text-[10px] font-bold rounded-full uppercase tracking-tighter';
1067
+ }
1068
+
1069
+ document.getElementById('run-results-content').innerHTML =
1070
+ detailRow('Inference Time', d.processing_time + ' sec') +
1071
+ infoRow('Throughput (FPS)', d.actual_fps, 'Measured frame throughput during processing.', ' <span class="text-xs text-slate-400 font-normal">fps</span>') +
1072
+ infoRow('Real-time Ratio', d.speed_vs_realtime + 'x', 'Processing speed relative to video playback rate.');
1073
 
1074
  if (d.video_id) loadReports(d.video_id);
1075
  return;
 
1083
  const totalIn = sumValues(d.class_in);
1084
  const totalOut = sumValues(d.class_out);
1085
  document.getElementById('cnt-total').innerText = totalIn + totalOut;
1086
+
1087
+ // Update doughnut
1088
+ doughChart.data.datasets[0].data = [totalIn, totalOut];
1089
+ doughChart.update();
1090
 
1091
  const now = performance.now();
1092
  if (now - lastUIUpdate < 300) return;
 
1101
 
1102
  const REPORT_LABELS = {
1103
  'direction_pie.png': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
1104
+ 'direction_pie.pdf': { title: 'Directional Split', desc: 'Incoming vs outgoing vehicle ratio' },
1105
  'flow_over_time.png': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
1106
+ 'flow_over_time.pdf': { title: 'Traffic Flow', desc: 'Vehicles crossing the line over time' },
1107
  'congestion_index.png': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
1108
+ 'congestion_index.pdf': { title: 'Congestion Index', desc: 'Active vehicles per frame with smoothing' },
1109
  'class_dominance.png': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1110
+ 'class_dominance.pdf': { title: 'Class Dominance', desc: 'Vehicle count by classification type' },
1111
  'confidence_dist.png': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
1112
+ 'confidence_dist.pdf': { title: 'Confidence Distribution', desc: 'Detection confidence histogram' },
1113
+ 'annotated.mp4': { title: 'Annotated Video', desc: 'Video with bounding boxes, track IDs, and spatial boundary overlays' },
1114
  };
1115
 
1116
  async function loadReports(videoId) {
 
1126
  data.files.forEach(name => {
1127
  const info = REPORT_LABELS[name] || { title: name, desc: '' };
1128
  const url = `/reports/${videoId}/${name}`;
1129
+ const isVideo = name.endsWith('.mp4');
1130
+ const isPDF = name.endsWith('.pdf');
1131
  const card = document.createElement('div');
1132
  card.className = 'bg-white rounded-xl border border-slate-200 shadow-sm flex flex-col overflow-hidden';
1133
+
1134
+ let previewHTML = '';
1135
+ if (isVideo) {
1136
+ previewHTML = `
1137
+ <div class="flex flex-col items-center justify-center py-12 text-slate-300">
1138
+ <i class="fa-solid fa-film text-6xl mb-4"></i>
1139
+ <span class="text-xs font-bold uppercase tracking-widest text-slate-400">Video Ready for Local Analysis</span>
1140
+ </div>`;
1141
+ } else if (isPDF) {
1142
+ previewHTML = `
1143
+ <div class="flex flex-col items-center justify-center py-12 text-slate-300">
1144
+ <i class="fa-solid fa-file-pdf text-6xl mb-4"></i>
1145
+ <span class="text-xs font-bold uppercase tracking-widest text-slate-400">PDF Document</span>
1146
+ </div>`;
1147
+ } else {
1148
+ previewHTML = `<img src="${url}" alt="${info.title}" class="max-w-full max-h-[320px] object-contain rounded">`;
1149
+ }
1150
+
1151
  card.innerHTML = `
1152
  <div class="px-5 py-3 border-b border-slate-100 bg-slate-50/50 flex justify-between items-center">
1153
  <div>
 
1160
  </a>
1161
  </div>
1162
  <div class="p-4 flex items-center justify-center bg-slate-50/30">
1163
+ ${previewHTML}
1164
  </div>
1165
  `;
1166
  grid.appendChild(card);