cyberai-1 commited on
Commit
4435ba4
Β·
1 Parent(s): 0507108
.gitignore ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+
4
+ # Generated runtime outputs
5
+ data/*.mp4
6
+ data/*_annotated.mp4
7
+ logs/
8
+ output/
9
+ uploads/
backend/__pycache__/main.cpython-311.pyc DELETED
Binary file (23.1 kB)
 
backend/dataset.yaml CHANGED
@@ -17,18 +17,4 @@ names:
17
  2: car
18
  3: motorbike
19
  5: bus
20
- 7: truck
21
-
22
- # ── Notes ──────────────────────────────────────────────────────────────────
23
- # When exporting from Roboflow or Label Studio, remap your class indices
24
- # to match the COCO indices above (0,1,2,3,5,7) if needed.
25
- #
26
- # Recommended dataset size per scene:
27
- # - β‰₯ 500 annotated frames
28
- # - Mix of day/night, weather conditions
29
- # - Multiple angles and zoom levels
30
- #
31
- # Labeling tips:
32
- # - Label ALL visible objects, including partially occluded ones
33
- # - Use tight bounding boxes
34
- # - Consistent class names (lowercase, no spaces)
 
17
  2: car
18
  3: motorbike
19
  5: bus
20
+ 7: truck
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/main.py CHANGED
@@ -11,6 +11,8 @@ import subprocess
11
  import time
12
  import uuid
13
  import threading
 
 
14
  from collections import defaultdict, deque
15
  from pathlib import Path
16
 
@@ -107,7 +109,8 @@ async def get_classes():
107
 
108
  @app.post("/upload")
109
  async def upload_video(
110
- file: UploadFile = File(...),
 
111
  scene_name: str = Form("scene_01"),
112
  group_id: str = Form("Group_05"),
113
  classes: str = Form(""),
@@ -116,10 +119,27 @@ async def upload_video(
116
  ):
117
  sid = str(uuid.uuid4())[:12]
118
 
119
- suffix = Path(file.filename).suffix or ".mp4"
120
- video_name = file.filename or f"{sid}{suffix}"
121
- video_path = UPLOAD_DIR / f"{sid}{suffix}"
122
- video_path.write_bytes(await file.read())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  selected = [c.strip() for c in classes.split(",") if c.strip()] or DEFAULT_CLASSES
125
 
 
11
  import time
12
  import uuid
13
  import threading
14
+ import urllib.parse
15
+ import urllib.request
16
  from collections import defaultdict, deque
17
  from pathlib import Path
18
 
 
109
 
110
  @app.post("/upload")
111
  async def upload_video(
112
+ file: UploadFile | None = File(None),
113
+ video_url: str = Form(""),
114
  scene_name: str = Form("scene_01"),
115
  group_id: str = Form("Group_05"),
116
  classes: str = Form(""),
 
119
  ):
120
  sid = str(uuid.uuid4())[:12]
121
 
122
+ if file is not None and file.filename:
123
+ suffix = Path(file.filename).suffix or ".mp4"
124
+ video_name = file.filename
125
+ video_path = UPLOAD_DIR / f"{sid}{suffix}"
126
+ video_path.write_bytes(await file.read())
127
+ elif video_url.strip():
128
+ parsed = urllib.parse.urlparse(video_url.strip())
129
+ if parsed.scheme not in ("http", "https"):
130
+ raise HTTPException(400, "Video URL must start with http:// or https://")
131
+
132
+ suffix = Path(parsed.path).suffix or ".mp4"
133
+ video_name = Path(parsed.path).name or f"{sid}{suffix}"
134
+ video_path = UPLOAD_DIR / f"{sid}{suffix}"
135
+ try:
136
+ with urllib.request.urlopen(video_url.strip(), timeout=30) as response:
137
+ with open(video_path, "wb") as out:
138
+ shutil.copyfileobj(response, out)
139
+ except Exception as exc:
140
+ raise HTTPException(400, f"Could not download video URL: {exc}") from exc
141
+ else:
142
+ raise HTTPException(400, "Upload a video file or provide a video URL")
143
 
144
  selected = [c.strip() for c in classes.split(",") if c.strip()] or DEFAULT_CLASSES
145
 
data/Africa_Countries_20260428_135610_detections.csv ADDED
The diff for this file is too large to render. See raw diff
 
frontend/index.html CHANGED
@@ -169,6 +169,10 @@
169
  padding: 7px 10px; outline: none; border-radius: 2px; transition: border-color .2s;
170
  }
171
  .field input:focus, .field select:focus { border-color: var(--accent); }
 
 
 
 
172
 
173
  /* Class chips */
174
  .class-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
@@ -274,6 +278,11 @@
274
  /* ── DASHBOARD PANEL ── */
275
  #view-dashboard { flex: 1; overflow-y: auto; padding: 22px; display: none; }
276
 
 
 
 
 
 
277
  .dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; margin-bottom: 22px; }
278
  .stat-card { background: var(--surface); border: 1px solid var(--border); padding: 18px; border-radius: 2px; }
279
  .sc-label { font-family: var(--mono); font-size: 10px; color: var(--dim); letter-spacing: 1.5px; margin-bottom: 8px; }
@@ -418,6 +427,10 @@
418
  <div class="up-hint"><b>Drop video here</b><br>or click to browse</div>
419
  <div class="up-name" id="fileName"></div>
420
  </label>
 
 
 
 
421
  <!-- Progress bar for upload -->
422
  <div id="uploadProgBar"><div id="uploadProgFill"></div></div>
423
  <div id="uploadProgLabel"></div>
@@ -502,9 +515,17 @@
502
 
503
  <!-- DASHBOARD -->
504
  <div id="view-dashboard">
 
 
 
 
 
 
 
 
505
  <div class="dash-grid" id="dashStats"></div>
506
  <div class="chart-card">
507
- <h3>Objects by Class β€” All Scenes</h3>
508
  <div class="bar-chart" id="barChart"></div>
509
  </div>
510
  <div class="chart-card">
@@ -512,7 +533,7 @@
512
  <canvas id="timelineCanvas"></canvas>
513
  </div>
514
  <div class="chart-card">
515
- <h3>Scene Comparison</h3>
516
  <table class="scene-table">
517
  <thead>
518
  <tr>
@@ -584,6 +605,7 @@ let currentSid = null;
584
  let sseSource = null;
585
  let selectedFile = null;
586
  let pollTimer = null;
 
587
 
588
  // Canvas
589
  const canvas = document.getElementById('liveCanvas');
@@ -610,6 +632,15 @@ window.onload = () => {
610
  document.getElementById('videoFile').addEventListener('change', e => {
611
  selectedFile = e.target.files[0];
612
  document.getElementById('fileName').textContent = selectedFile?.name || '';
 
 
 
 
 
 
 
 
 
613
  });
614
 
615
  const zone = document.getElementById('uploadZone');
@@ -620,6 +651,7 @@ window.onload = () => {
620
  if (e.dataTransfer.files[0]) {
621
  selectedFile = e.dataTransfer.files[0];
622
  document.getElementById('fileName').textContent = selectedFile.name;
 
623
  }
624
  });
625
  };
@@ -667,13 +699,15 @@ function switchTab(tab, btn) {
667
 
668
  // ══ PROCESSING ════════════════════════════════════════════════════════════════
669
  async function startProcessing() {
670
- if (!selectedFile) { toast('Please upload a video file first.', 'err'); return; }
 
671
 
672
  const selected = [...document.querySelectorAll('#classGrid input:checked')].map(i => i.value);
673
  if (!selected.length) { toast('Select at least one class.', 'err'); return; }
674
 
675
  const fd = new FormData();
676
- fd.append('file', selectedFile);
 
677
  fd.append('scene_name', document.getElementById('sceneName').value || 'scene_01');
678
  fd.append('group_id', document.getElementById('groupId').value || 'group_01');
679
  fd.append('classes', selected.join(','));
@@ -690,7 +724,7 @@ async function startProcessing() {
690
  progBar.style.display = 'block';
691
  progFill.style.width = '0%';
692
  progLabel.style.display = 'block';
693
- progLabel.textContent = 'Uploading...';
694
 
695
  try {
696
  // Use XMLHttpRequest for progress
@@ -719,7 +753,7 @@ async function startProcessing() {
719
  reject(new Error('Network error'));
720
  };
721
  xhr.upload.onprogress = function(e) {
722
- if (e.lengthComputable) {
723
  const pct = Math.round(e.loaded / e.total * 100);
724
  progFill.style.width = pct + '%';
725
  progLabel.textContent = `Uploading... ${pct}%`;
@@ -968,16 +1002,55 @@ async function loadDashboard() {
968
  try {
969
  const r = await fetch(`${API_BASE}/dashboard`);
970
  const d = await r.json();
971
- renderDashboard(d);
 
 
972
  } catch {
973
  document.getElementById('dashStats').innerHTML =
974
  '<div style="font-family:var(--mono);font-size:12px;color:var(--dim)">Server not reachable.</div>';
975
  }
976
  }
977
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
978
  function renderDashboard(d) {
979
  const { global_counts = {}, total_scenes = 0, total_objects = 0, scenes = [] } = d;
980
  const totalDur = scenes.reduce((s, sc) => s + (sc.duration_sec || 0), 0);
 
 
 
 
981
 
982
  document.getElementById('dashStats').innerHTML = [
983
  statCard('SCENES', total_scenes, 'videos'),
 
169
  padding: 7px 10px; outline: none; border-radius: 2px; transition: border-color .2s;
170
  }
171
  .field input:focus, .field select:focus { border-color: var(--accent); }
172
+ .source-divider {
173
+ font-family: var(--mono); font-size: 11px; color: var(--dim);
174
+ text-align: center; margin: 8px 0; letter-spacing: 1px;
175
+ }
176
 
177
  /* Class chips */
178
  .class-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
 
278
  /* ── DASHBOARD PANEL ── */
279
  #view-dashboard { flex: 1; overflow-y: auto; padding: 22px; display: none; }
280
 
281
+ .dash-toolbar {
282
+ display: flex; justify-content: flex-end; margin-bottom: 14px;
283
+ }
284
+ .dash-toolbar .field { width: min(260px, 100%); margin-bottom: 0; }
285
+
286
  .dash-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 14px; margin-bottom: 22px; }
287
  .stat-card { background: var(--surface); border: 1px solid var(--border); padding: 18px; border-radius: 2px; }
288
  .sc-label { font-family: var(--mono); font-size: 10px; color: var(--dim); letter-spacing: 1.5px; margin-bottom: 8px; }
 
427
  <div class="up-hint"><b>Drop video here</b><br>or click to browse</div>
428
  <div class="up-name" id="fileName"></div>
429
  </label>
430
+ <div class="source-divider">OR VIDEO URL</div>
431
+ <div class="field">
432
+ <input type="url" id="videoUrl" placeholder="https://example.com/video.mp4"/>
433
+ </div>
434
  <!-- Progress bar for upload -->
435
  <div id="uploadProgBar"><div id="uploadProgFill"></div></div>
436
  <div id="uploadProgLabel"></div>
 
515
 
516
  <!-- DASHBOARD -->
517
  <div id="view-dashboard">
518
+ <div class="dash-toolbar">
519
+ <div class="field">
520
+ <label>FILTER BY SCENE</label>
521
+ <select id="sceneFilter" onchange="applySceneFilter()">
522
+ <option value="">All scenes</option>
523
+ </select>
524
+ </div>
525
+ </div>
526
  <div class="dash-grid" id="dashStats"></div>
527
  <div class="chart-card">
528
+ <h3 id="objectsChartTitle">Objects by Class - All Scenes</h3>
529
  <div class="bar-chart" id="barChart"></div>
530
  </div>
531
  <div class="chart-card">
 
533
  <canvas id="timelineCanvas"></canvas>
534
  </div>
535
  <div class="chart-card">
536
+ <h3 id="sceneComparisonTitle">Scene Comparison</h3>
537
  <table class="scene-table">
538
  <thead>
539
  <tr>
 
605
  let sseSource = null;
606
  let selectedFile = null;
607
  let pollTimer = null;
608
+ let dashboardCache = null;
609
 
610
  // Canvas
611
  const canvas = document.getElementById('liveCanvas');
 
632
  document.getElementById('videoFile').addEventListener('change', e => {
633
  selectedFile = e.target.files[0];
634
  document.getElementById('fileName').textContent = selectedFile?.name || '';
635
+ if (selectedFile) document.getElementById('videoUrl').value = '';
636
+ });
637
+
638
+ document.getElementById('videoUrl').addEventListener('input', e => {
639
+ if (e.target.value.trim()) {
640
+ selectedFile = null;
641
+ document.getElementById('videoFile').value = '';
642
+ document.getElementById('fileName').textContent = '';
643
+ }
644
  });
645
 
646
  const zone = document.getElementById('uploadZone');
 
651
  if (e.dataTransfer.files[0]) {
652
  selectedFile = e.dataTransfer.files[0];
653
  document.getElementById('fileName').textContent = selectedFile.name;
654
+ document.getElementById('videoUrl').value = '';
655
  }
656
  });
657
  };
 
699
 
700
  // ══ PROCESSING ════════════════════════════════════════════════════════════════
701
  async function startProcessing() {
702
+ const videoUrl = document.getElementById('videoUrl').value.trim();
703
+ if (!selectedFile && !videoUrl) { toast('Upload a video file or enter a video URL.', 'err'); return; }
704
 
705
  const selected = [...document.querySelectorAll('#classGrid input:checked')].map(i => i.value);
706
  if (!selected.length) { toast('Select at least one class.', 'err'); return; }
707
 
708
  const fd = new FormData();
709
+ if (selectedFile) fd.append('file', selectedFile);
710
+ else fd.append('video_url', videoUrl);
711
  fd.append('scene_name', document.getElementById('sceneName').value || 'scene_01');
712
  fd.append('group_id', document.getElementById('groupId').value || 'group_01');
713
  fd.append('classes', selected.join(','));
 
724
  progBar.style.display = 'block';
725
  progFill.style.width = '0%';
726
  progLabel.style.display = 'block';
727
+ progLabel.textContent = selectedFile ? 'Uploading...' : 'Fetching URL...';
728
 
729
  try {
730
  // Use XMLHttpRequest for progress
 
753
  reject(new Error('Network error'));
754
  };
755
  xhr.upload.onprogress = function(e) {
756
+ if (selectedFile && e.lengthComputable) {
757
  const pct = Math.round(e.loaded / e.total * 100);
758
  progFill.style.width = pct + '%';
759
  progLabel.textContent = `Uploading... ${pct}%`;
 
1002
  try {
1003
  const r = await fetch(`${API_BASE}/dashboard`);
1004
  const d = await r.json();
1005
+ dashboardCache = d;
1006
+ populateSceneFilter(d.scenes || []);
1007
+ applySceneFilter();
1008
  } catch {
1009
  document.getElementById('dashStats').innerHTML =
1010
  '<div style="font-family:var(--mono);font-size:12px;color:var(--dim)">Server not reachable.</div>';
1011
  }
1012
  }
1013
 
1014
+ function populateSceneFilter(scenes) {
1015
+ const select = document.getElementById('sceneFilter');
1016
+ const current = select.value;
1017
+ const names = [...new Set(scenes.map(sc => sc.scene).filter(Boolean))].sort();
1018
+ select.innerHTML = '<option value="">All scenes</option>' +
1019
+ names.map(name => `<option value="${escapeHtml(name)}">${escapeHtml(name)}</option>`).join('');
1020
+ if (names.includes(current)) select.value = current;
1021
+ }
1022
+
1023
+ function applySceneFilter() {
1024
+ if (!dashboardCache) return;
1025
+ const scene = document.getElementById('sceneFilter').value;
1026
+ const scenes = (dashboardCache.scenes || []).filter(sc => !scene || sc.scene === scene);
1027
+ const global_counts = {};
1028
+ for (const sc of scenes) {
1029
+ for (const [cls, cnt] of Object.entries(sc.count_per_class || {})) {
1030
+ global_counts[cls] = (global_counts[cls] || 0) + cnt;
1031
+ }
1032
+ }
1033
+ renderDashboard({
1034
+ scenes,
1035
+ global_counts,
1036
+ total_scenes: scenes.length,
1037
+ total_objects: Object.values(global_counts).reduce((a, b) => a + b, 0),
1038
+ });
1039
+ }
1040
+
1041
+ function escapeHtml(value) {
1042
+ return String(value).replace(/[&<>"']/g, ch => ({
1043
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
1044
+ }[ch]));
1045
+ }
1046
+
1047
  function renderDashboard(d) {
1048
  const { global_counts = {}, total_scenes = 0, total_objects = 0, scenes = [] } = d;
1049
  const totalDur = scenes.reduce((s, sc) => s + (sc.duration_sec || 0), 0);
1050
+ const selectedScene = document.getElementById('sceneFilter')?.value || '';
1051
+ const scope = selectedScene ? selectedScene : 'All Scenes';
1052
+ document.getElementById('objectsChartTitle').textContent = `Objects by Class - ${scope}`;
1053
+ document.getElementById('sceneComparisonTitle').textContent = selectedScene ? 'Selected Scene' : 'Scene Comparison';
1054
 
1055
  document.getElementById('dashStats').innerHTML = [
1056
  statCard('SCENES', total_scenes, 'videos'),