Spaces:
Sleeping
Sleeping
cyberai-1 commited on
Commit Β·
4435ba4
1
Parent(s): 0507108
Add
Browse files- .gitignore +9 -0
- backend/__pycache__/main.cpython-311.pyc +0 -0
- backend/dataset.yaml +1 -15
- backend/main.py +25 -5
- data/Africa_Countries_20260428_135610_detections.csv +0 -0
- frontend/index.html +80 -7
.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 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
|
|
|
| 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',
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
| 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'),
|