Spaces:
Paused
Paused
Zhen Ye Claude Opus 4.6 (1M context) commited on
Commit ·
165863b
1
Parent(s): 9574811
refactor: replace frontend SPA with minimal detection UI
Browse filesDelete the full Mission Console frontend (frontend/ directory) and
replace with a single index.html — vanilla HTML/JS with video upload,
mode/model picker, live MJPEG stream, processed video playback,
and collapsible detection JSON viewer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- frontend/data/helicopter_demo_data.js +0 -38
- frontend/data/helicopter_demo_tracks.json +0 -38
- frontend/index.html +0 -302
- frontend/js/api/client.js +0 -372
- frontend/js/core/config.js +0 -15
- frontend/js/core/demo.js +0 -142
- frontend/js/core/gptMapping.js +0 -46
- frontend/js/core/hel.js +0 -245
- frontend/js/core/physics.js +0 -57
- frontend/js/core/state.js +0 -80
- frontend/js/core/tracker.js +0 -278
- frontend/js/core/utils.js +0 -55
- frontend/js/core/video.js +0 -621
- frontend/js/init.js +0 -6
- frontend/js/main.js +0 -785
- frontend/js/ui/cards.js +0 -130
- frontend/js/ui/chat.js +0 -155
- frontend/js/ui/cursor.js +0 -90
- frontend/js/ui/logging.js +0 -45
- frontend/js/ui/overlays.js +0 -113
- frontend/style.css +0 -1142
- index.html +197 -0
frontend/data/helicopter_demo_data.js
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
window.HELICOPTER_DEMO_DATA = {
|
| 2 |
-
"fps": 24,
|
| 3 |
-
"totalFrames": 192,
|
| 4 |
-
"video": "Enhance_Video_Movement.mp4",
|
| 5 |
-
"format": "keyframes",
|
| 6 |
-
"keyframes": {
|
| 7 |
-
"0": [
|
| 8 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 200, "y": 260, "w": 180, "h": 120}, "gpt_distance_m": 800, "angle_deg": -90, "speed_kph": 180},
|
| 9 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 420, "y": 240, "w": 150, "h": 100}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 180},
|
| 10 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 620, "y": 250, "w": 100, "h": 68}, "gpt_distance_m": 1200, "angle_deg": 90, "speed_kph": 185},
|
| 11 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 850, "y": 235, "w": 90, "h": 62}, "gpt_distance_m": 1300, "angle_deg": 90, "speed_kph": 190}
|
| 12 |
-
],
|
| 13 |
-
"48": [
|
| 14 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 150, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 700, "angle_deg": -90, "speed_kph": 190},
|
| 15 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 400, "y": 255, "w": 160, "h": 105}, "gpt_distance_m": 850, "angle_deg": 90, "speed_kph": 185},
|
| 16 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 640, "y": 255, "w": 115, "h": 78}, "gpt_distance_m": 1050, "angle_deg": 90, "speed_kph": 188},
|
| 17 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 870, "y": 245, "w": 105, "h": 72}, "gpt_distance_m": 1150, "angle_deg": 90, "speed_kph": 192}
|
| 18 |
-
],
|
| 19 |
-
"96": [
|
| 20 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 80, "y": 300, "w": 220, "h": 145}, "gpt_distance_m": 600, "angle_deg": -90, "speed_kph": 200},
|
| 21 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 380, "y": 265, "w": 155, "h": 102}, "gpt_distance_m": 820, "angle_deg": 90, "speed_kph": 182},
|
| 22 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 660, "y": 262, "w": 135, "h": 92}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 190},
|
| 23 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 890, "y": 258, "w": 125, "h": 85}, "gpt_distance_m": 980, "angle_deg": 90, "speed_kph": 195}
|
| 24 |
-
],
|
| 25 |
-
"144": [
|
| 26 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 50, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 650, "angle_deg": -90, "speed_kph": 195},
|
| 27 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 360, "y": 270, "w": 148, "h": 98}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 178},
|
| 28 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 680, "y": 272, "w": 158, "h": 108}, "gpt_distance_m": 750, "angle_deg": 90, "speed_kph": 192},
|
| 29 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 910, "y": 270, "w": 148, "h": 100}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 198}
|
| 30 |
-
],
|
| 31 |
-
"191": [
|
| 32 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 30, "y": 260, "w": 190, "h": 125}, "gpt_distance_m": 680, "angle_deg": -90, "speed_kph": 190},
|
| 33 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 340, "y": 275, "w": 142, "h": 94}, "gpt_distance_m": 780, "angle_deg": 90, "speed_kph": 175},
|
| 34 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 700, "y": 280, "w": 180, "h": 122}, "gpt_distance_m": 620, "angle_deg": 90, "speed_kph": 195},
|
| 35 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 930, "y": 282, "w": 172, "h": 116}, "gpt_distance_m": 650, "angle_deg": 90, "speed_kph": 200}
|
| 36 |
-
]
|
| 37 |
-
}
|
| 38 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/data/helicopter_demo_tracks.json
DELETED
|
@@ -1,38 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"fps": 24,
|
| 3 |
-
"totalFrames": 192,
|
| 4 |
-
"video": "Enhance_Video_Movement.mp4",
|
| 5 |
-
"format": "keyframes",
|
| 6 |
-
"keyframes": {
|
| 7 |
-
"0": [
|
| 8 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 200, "y": 260, "w": 180, "h": 120}, "gpt_distance_m": 800, "angle_deg": -90, "speed_kph": 180},
|
| 9 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 420, "y": 240, "w": 150, "h": 100}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 180},
|
| 10 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 620, "y": 250, "w": 100, "h": 68}, "gpt_distance_m": 1200, "angle_deg": 90, "speed_kph": 185},
|
| 11 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 850, "y": 235, "w": 90, "h": 62}, "gpt_distance_m": 1300, "angle_deg": 90, "speed_kph": 190}
|
| 12 |
-
],
|
| 13 |
-
"48": [
|
| 14 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 150, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 700, "angle_deg": -90, "speed_kph": 190},
|
| 15 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 400, "y": 255, "w": 160, "h": 105}, "gpt_distance_m": 850, "angle_deg": 90, "speed_kph": 185},
|
| 16 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 640, "y": 255, "w": 115, "h": 78}, "gpt_distance_m": 1050, "angle_deg": 90, "speed_kph": 188},
|
| 17 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 870, "y": 245, "w": 105, "h": 72}, "gpt_distance_m": 1150, "angle_deg": 90, "speed_kph": 192}
|
| 18 |
-
],
|
| 19 |
-
"96": [
|
| 20 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 80, "y": 300, "w": 220, "h": 145}, "gpt_distance_m": 600, "angle_deg": -90, "speed_kph": 200},
|
| 21 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 380, "y": 265, "w": 155, "h": 102}, "gpt_distance_m": 820, "angle_deg": 90, "speed_kph": 182},
|
| 22 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 660, "y": 262, "w": 135, "h": 92}, "gpt_distance_m": 900, "angle_deg": 90, "speed_kph": 190},
|
| 23 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 890, "y": 258, "w": 125, "h": 85}, "gpt_distance_m": 980, "angle_deg": 90, "speed_kph": 195}
|
| 24 |
-
],
|
| 25 |
-
"144": [
|
| 26 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 50, "y": 280, "w": 200, "h": 130}, "gpt_distance_m": 650, "angle_deg": -90, "speed_kph": 195},
|
| 27 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 360, "y": 270, "w": 148, "h": 98}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 178},
|
| 28 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 680, "y": 272, "w": 158, "h": 108}, "gpt_distance_m": 750, "angle_deg": 90, "speed_kph": 192},
|
| 29 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 910, "y": 270, "w": 148, "h": 100}, "gpt_distance_m": 800, "angle_deg": 90, "speed_kph": 198}
|
| 30 |
-
],
|
| 31 |
-
"191": [
|
| 32 |
-
{"id": "H01", "label": "helicopter", "bbox": {"x": 30, "y": 260, "w": 190, "h": 125}, "gpt_distance_m": 680, "angle_deg": -90, "speed_kph": 190},
|
| 33 |
-
{"id": "H02", "label": "helicopter", "bbox": {"x": 340, "y": 275, "w": 142, "h": 94}, "gpt_distance_m": 780, "angle_deg": 90, "speed_kph": 175},
|
| 34 |
-
{"id": "H03", "label": "helicopter", "bbox": {"x": 700, "y": 280, "w": 180, "h": 122}, "gpt_distance_m": 620, "angle_deg": 90, "speed_kph": 195},
|
| 35 |
-
{"id": "H04", "label": "helicopter", "bbox": {"x": 930, "y": 282, "w": 172, "h": 116}, "gpt_distance_m": 650, "angle_deg": 90, "speed_kph": 200}
|
| 36 |
-
]
|
| 37 |
-
}
|
| 38 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/index.html
DELETED
|
@@ -1,302 +0,0 @@
|
|
| 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 |
-
<link rel="stylesheet" href="style.css">
|
| 8 |
-
<title>Mission Console</title>
|
| 9 |
-
</head>
|
| 10 |
-
|
| 11 |
-
<body>
|
| 12 |
-
<div id="app">
|
| 13 |
-
<header>
|
| 14 |
-
<div class="brand">
|
| 15 |
-
<div class="logo" aria-hidden="true"></div>
|
| 16 |
-
<div>
|
| 17 |
-
<h1>Mission Console</h1>
|
| 18 |
-
<div class="sub">Video → detection → analysis → tracking → mission assessment</div>
|
| 19 |
-
</div>
|
| 20 |
-
</div>
|
| 21 |
-
<div class="status-row">
|
| 22 |
-
<div class="pill">
|
| 23 |
-
<span class="dot" id="sys-dot"></span>
|
| 24 |
-
<span id="sys-status">STANDBY · No video loaded</span>
|
| 25 |
-
</div>
|
| 26 |
-
<div class="pill">
|
| 27 |
-
<span class="kbd">Detect</span>
|
| 28 |
-
<span>First-frame analysis</span>
|
| 29 |
-
</div>
|
| 30 |
-
<div class="pill">
|
| 31 |
-
<span class="kbd">Track</span>
|
| 32 |
-
<span>Continuous monitoring</span>
|
| 33 |
-
</div>
|
| 34 |
-
</div>
|
| 35 |
-
</header>
|
| 36 |
-
|
| 37 |
-
<div class="workspace">
|
| 38 |
-
<aside>
|
| 39 |
-
<div class="card">
|
| 40 |
-
<h2>Video Input</h2>
|
| 41 |
-
<div class="hint">Upload one video. Tab 1 uses only the first frame. Tab 2 reuses the same video for tracking
|
| 42 |
-
and monitoring.</div>
|
| 43 |
-
|
| 44 |
-
<div class="row mt-md">
|
| 45 |
-
<label for="videoFile">Video file</label>
|
| 46 |
-
<span class="badge"><span id="videoMeta">No file</span></span>
|
| 47 |
-
</div>
|
| 48 |
-
<input id="videoFile" type="file" accept="video/*" />
|
| 49 |
-
|
| 50 |
-
<div class="mt-md">
|
| 51 |
-
<label>Mission Objective (optional · enables class filtering)</label>
|
| 52 |
-
<textarea id="missionText" rows="3"
|
| 53 |
-
placeholder="Optional: e.g., Detect people and vehicles; highlight hazards and key objects."></textarea>
|
| 54 |
-
|
| 55 |
-
<div class="hint mt-sm">
|
| 56 |
-
Mission objective is <b>optional</b>. If provided, it will be used directly as input to the detector.
|
| 57 |
-
If left blank, the detector will detect <b>all</b> objects without filtering.
|
| 58 |
-
<div class="mini mt-xs" id="hfBackendStatus">HF Backend: STANDBY</div>
|
| 59 |
-
</div>
|
| 60 |
-
</div>
|
| 61 |
-
|
| 62 |
-
<div class="btnrow">
|
| 63 |
-
<button id="btnLoadSample" class="btn secondary" title="Optional: wire up sample videos later" disabled>Load
|
| 64 |
-
Sample</button>
|
| 65 |
-
<button id="btnEject" class="btn danger" title="Unload video">Eject</button>
|
| 66 |
-
</div>
|
| 67 |
-
|
| 68 |
-
<div class="grid2">
|
| 69 |
-
<div>
|
| 70 |
-
<label>Detector</label>
|
| 71 |
-
<select id="detectorSelect">
|
| 72 |
-
<optgroup label="Object Detection Models">
|
| 73 |
-
<option value="yolo11" data-kind="object" selected>Lite</option>
|
| 74 |
-
<option value="detr_resnet50" data-kind="object">Big</option>
|
| 75 |
-
<option value="grounding_dino" data-kind="object">Large</option>
|
| 76 |
-
</optgroup>
|
| 77 |
-
<optgroup label="Segmentation Models">
|
| 78 |
-
<option value="GSAM2-L" data-kind="segmentation">GSAM2-L</option>
|
| 79 |
-
<option value="GSAM2-B" data-kind="segmentation">GSAM2-B</option>
|
| 80 |
-
<option value="GSAM2-S" data-kind="segmentation">GSAM2-S</option>
|
| 81 |
-
<option value="YSAM2-L" data-kind="segmentation">YSAM2-L (Fast)</option>
|
| 82 |
-
<option value="YSAM2-B" data-kind="segmentation">YSAM2-B (Fast)</option>
|
| 83 |
-
<option value="YSAM2-S" data-kind="segmentation">YSAM2-S (Fast)</option>
|
| 84 |
-
</optgroup>
|
| 85 |
-
<optgroup label="Drone Detection Models">
|
| 86 |
-
<option value="drone_yolo" data-kind="drone">Drone</option>
|
| 87 |
-
</optgroup>
|
| 88 |
-
|
| 89 |
-
</select>
|
| 90 |
-
</div>
|
| 91 |
-
<div>
|
| 92 |
-
<label>Tracking</label>
|
| 93 |
-
<select id="trackerSelect">
|
| 94 |
-
<option value="iou">IOU + velocity (built-in)</option>
|
| 95 |
-
<option value="external">External hook (user API)</option>
|
| 96 |
-
</select>
|
| 97 |
-
</div>
|
| 98 |
-
|
| 99 |
-
<label class="checkbox-row" for="enableDepthToggle" style="display:none">
|
| 100 |
-
<input type="checkbox" id="enableDepthToggle">
|
| 101 |
-
<span>Enable Legacy Depth Map (Slow)</span>
|
| 102 |
-
</label>
|
| 103 |
-
<label class="checkbox-row" for="enableGPTToggle" style="margin-top: 4px;">
|
| 104 |
-
<input type="checkbox" id="enableGPTToggle" checked>
|
| 105 |
-
<span style="color: var(--accent-light);">Enable GPT Reasoning</span>
|
| 106 |
-
</label>
|
| 107 |
-
<label class="checkbox-row" for="enableStreamToggle" style="margin-top: 4px;">
|
| 108 |
-
<input type="checkbox" id="enableStreamToggle" checked>
|
| 109 |
-
<span>Enable Stream Processing</span>
|
| 110 |
-
</label>
|
| 111 |
-
</div>
|
| 112 |
-
|
| 113 |
-
</div>
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
<div class="card" style="flex:1; min-height:0">
|
| 117 |
-
<h2>System Log</h2>
|
| 118 |
-
<div class="log" id="sysLog"></div>
|
| 119 |
-
</div>
|
| 120 |
-
</aside>
|
| 121 |
-
|
| 122 |
-
<main>
|
| 123 |
-
<div class="tabs">
|
| 124 |
-
<button class="tabbtn active" data-tab="frame">Tab 1 · Detect & Analyze</button>
|
| 125 |
-
<button class="tabbtn" data-tab="engage">Tab 2 · Track & Monitor</button>
|
| 126 |
-
</div>
|
| 127 |
-
|
| 128 |
-
<!-- ===== Tab 1 ===== -->
|
| 129 |
-
<section class="tab active" id="tab-frame">
|
| 130 |
-
<div class="frame-grid">
|
| 131 |
-
<div class="panel panel-monitor">
|
| 132 |
-
<h3>
|
| 133 |
-
<span>First Frame · Detection + Analysis</span>
|
| 134 |
-
<span class="rightnote" id="frameNote">Awaiting video</span>
|
| 135 |
-
</h3>
|
| 136 |
-
<div class="viewbox" id="frameViewBox">
|
| 137 |
-
<canvas id="frameCanvas" width="1280" height="720"></canvas>
|
| 138 |
-
<canvas id="frameOverlay" class="overlay" width="1280" height="720"></canvas>
|
| 139 |
-
<div class="watermark">EO/IR · Track-ID · Classification</div>
|
| 140 |
-
<div class="empty" id="frameEmpty">
|
| 141 |
-
<div class="big">Upload a video to begin</div>
|
| 142 |
-
<div class="small">This demo performs first-frame detection and analysis. Then it replays the video
|
| 143 |
-
with continuous object tracking.</div>
|
| 144 |
-
<div style="display:flex; gap:10px; margin-top:6px; flex-wrap:wrap; justify-content:center;">
|
| 145 |
-
<span class="badge"><span class="dot"></span> If you are online, COCO-SSD loads automatically</span>
|
| 146 |
-
</div>
|
| 147 |
-
</div>
|
| 148 |
-
</div>
|
| 149 |
-
|
| 150 |
-
<div class="btnrow" style="margin-top:10px">
|
| 151 |
-
<button id="btnReason" class="btn">Detect</button>
|
| 152 |
-
<button id="btnCancelReason" class="btn danger" style="display: none;">Cancel</button>
|
| 153 |
-
<button id="btnRecompute" class="btn secondary">Reanalyze</button>
|
| 154 |
-
<button id="btnClear" class="btn secondary">Clear</button>
|
| 155 |
-
</div>
|
| 156 |
-
|
| 157 |
-
<div class="strip mt-md">
|
| 158 |
-
<span class="chip" id="chipFrameDepth"
|
| 159 |
-
title="Toggle depth view of first frame (if available)" style="display:none">VIEW:DEFAULT</span>
|
| 160 |
-
</div>
|
| 161 |
-
</div>
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
<div class="panel panel-summary" style="display:flex; flex-direction:column; min-height: 0; overflow: hidden;">
|
| 166 |
-
<h3>
|
| 167 |
-
<span>Object Track Cards</span>
|
| 168 |
-
<span class="rightnote" id="trackCount">0</span>
|
| 169 |
-
</h3>
|
| 170 |
-
<div class="list" id="frameTrackList" style="flex:1; overflow-y:auto; padding:6px; max-height:none;">
|
| 171 |
-
<!-- Cards injected here -->
|
| 172 |
-
<div style="font-style:italic; color:var(--faint); text-align:center; margin-top:20px; font-size:12px;">
|
| 173 |
-
No objects tracked.
|
| 174 |
-
</div>
|
| 175 |
-
</div>
|
| 176 |
-
</div>
|
| 177 |
-
|
| 178 |
-
<!-- Threat Chat Panel -->
|
| 179 |
-
<div class="panel panel-chat" id="chatPanel">
|
| 180 |
-
<h3>
|
| 181 |
-
<span>Mission Analyst</span>
|
| 182 |
-
<button class="collapse-btn" id="chatToggle" style="font-size: 0.75rem;">▲ Close Chat</button>
|
| 183 |
-
</h3>
|
| 184 |
-
<div class="chat-container">
|
| 185 |
-
<div class="chat-messages" id="chatMessages">
|
| 186 |
-
<div class="chat-message chat-system">
|
| 187 |
-
<span class="chat-icon">SYS</span>
|
| 188 |
-
<span class="chat-content">Run detection first, then ask questions about detected objects.</span>
|
| 189 |
-
</div>
|
| 190 |
-
</div>
|
| 191 |
-
<div class="chat-input-row">
|
| 192 |
-
<input type="text" id="chatInput" placeholder="Ask about detections..." autocomplete="off">
|
| 193 |
-
<button id="chatSend" class="btn">Send</button>
|
| 194 |
-
</div>
|
| 195 |
-
</div>
|
| 196 |
-
</div>
|
| 197 |
-
|
| 198 |
-
</div>
|
| 199 |
-
|
| 200 |
-
</section>
|
| 201 |
-
|
| 202 |
-
<!-- ===== Tab 2 ===== -->
|
| 203 |
-
<section class="tab" id="tab-engage">
|
| 204 |
-
<div class="engage-grid">
|
| 205 |
-
<div class="panel">
|
| 206 |
-
<h3>
|
| 207 |
-
<span>Video Tracking · Continuous Monitoring</span>
|
| 208 |
-
<div style="display: flex; gap: 8px; align-items: center;">
|
| 209 |
-
<button class="collapse-btn" id="btnToggleSidebar">◀ Hide Sidebar</button>
|
| 210 |
-
<span class="rightnote" id="engageNote">Awaiting video</span>
|
| 211 |
-
</div>
|
| 212 |
-
</h3>
|
| 213 |
-
|
| 214 |
-
<div class="viewbox" style="min-height: 420px;">
|
| 215 |
-
<video id="videoEngage" playsinline muted></video>
|
| 216 |
-
<canvas id="engageOverlay" class="overlay"></canvas>
|
| 217 |
-
<div class="watermark">TRACK · ID · DIST · STATUS</div>
|
| 218 |
-
<div class="empty" id="engageEmpty">
|
| 219 |
-
<div class="big">No video loaded</div>
|
| 220 |
-
<div class="small">Upload a video. Run <b>Detect</b> first to analyze objects.
|
| 221 |
-
Then click <b>Track</b>.</div>
|
| 222 |
-
</div>
|
| 223 |
-
</div>
|
| 224 |
-
|
| 225 |
-
<div class="btnrow mt-md">
|
| 226 |
-
<button id="btnEngage" class="btn">Track</button>
|
| 227 |
-
<button id="btnPause" class="btn secondary">Pause</button>
|
| 228 |
-
<button id="btnReset" class="btn secondary">Reset</button>
|
| 229 |
-
</div>
|
| 230 |
-
|
| 231 |
-
<div class="strip mt-md">
|
| 232 |
-
<span class="chip" id="chipPolicy">POLICY:AUTO</span>
|
| 233 |
-
<span class="chip" id="chipTracks">TRACKS:0</span>
|
| 234 |
-
<span class="chip" id="chipBeam">MODE:AUTO</span>
|
| 235 |
-
<span class="chip" id="chipHz">DET:6Hz</span>
|
| 236 |
-
<span class="chip" id="chipFeed" title="Toggle raw vs HF-processed feed (if available)">FEED:RAW</span>
|
| 237 |
-
<span class="chip" id="chipDepth" title="Toggle depth view (if available)" style="display:none">VIEW:DEFAULT</span>
|
| 238 |
-
</div>
|
| 239 |
-
|
| 240 |
-
<div class="mt-md">
|
| 241 |
-
<div class="row"><label>Active track progress (selected)</label><small class="mini"
|
| 242 |
-
id="dwellText">—</small>
|
| 243 |
-
</div>
|
| 244 |
-
<div class="bar">
|
| 245 |
-
<div id="dwellBar"></div>
|
| 246 |
-
</div>
|
| 247 |
-
</div>
|
| 248 |
-
|
| 249 |
-
<div class="hint mt-md">Manual selection: choose "Manual" in Policy, then click a target in the video.</div>
|
| 250 |
-
</div>
|
| 251 |
-
|
| 252 |
-
<div class="engage-right">
|
| 253 |
-
<div class="panel" style="flex:1; min-height:0">
|
| 254 |
-
<h3>
|
| 255 |
-
<span>Live Track Cards</span>
|
| 256 |
-
<span class="rightnote" id="liveStamp">—</span>
|
| 257 |
-
</h3>
|
| 258 |
-
<div class="list" id="trackList" style="max-height:none"></div>
|
| 259 |
-
</div>
|
| 260 |
-
</div>
|
| 261 |
-
</div>
|
| 262 |
-
</section>
|
| 263 |
-
|
| 264 |
-
</div>
|
| 265 |
-
|
| 266 |
-
<footer>
|
| 267 |
-
<div>Mission Console · Unclassified visuals</div>
|
| 268 |
-
<div class="mono" id="telemetry">VIS=16km · DET=6Hz</div>
|
| 269 |
-
</footer>
|
| 270 |
-
|
| 271 |
-
<!-- Hidden video used only for first-frame capture -->
|
| 272 |
-
<video id="videoHidden" playsinline muted style="display:none"></video>
|
| 273 |
-
</div>
|
| 274 |
-
|
| 275 |
-
<script>
|
| 276 |
-
window.API_CONFIG = {
|
| 277 |
-
BACKEND_BASE: "https://biaslab2025-perception.hf.space"
|
| 278 |
-
};
|
| 279 |
-
</script>
|
| 280 |
-
<script src="./js/init.js"></script>
|
| 281 |
-
<script src="./js/core/config.js"></script>
|
| 282 |
-
<script src="./js/core/utils.js"></script>
|
| 283 |
-
<script src="./js/core/state.js"></script>
|
| 284 |
-
<script src="./js/core/physics.js"></script>
|
| 285 |
-
<script src="./js/core/video.js"></script>
|
| 286 |
-
<script src="./js/core/hel.js"></script>
|
| 287 |
-
<script src="./js/ui/logging.js"></script>
|
| 288 |
-
<script src="./js/core/gptMapping.js"></script>
|
| 289 |
-
<script src="./js/core/tracker.js"></script>
|
| 290 |
-
<script src="./js/api/client.js"></script>
|
| 291 |
-
<script src="./js/ui/overlays.js"></script>
|
| 292 |
-
<script src="./js/ui/cards.js"></script>
|
| 293 |
-
<script src="./js/ui/features.js"></script>
|
| 294 |
-
<script src="./js/ui/cursor.js"></script>
|
| 295 |
-
<script src="./js/ui/chat.js"></script>
|
| 296 |
-
<script src="./data/helicopter_demo_data.js"></script>
|
| 297 |
-
<script src="./js/core/demo.js"></script>
|
| 298 |
-
<script src="./js/main.js"></script>
|
| 299 |
-
|
| 300 |
-
</body>
|
| 301 |
-
|
| 302 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/api/client.js
DELETED
|
@@ -1,372 +0,0 @@
|
|
| 1 |
-
// API Client Module - Backend communication
|
| 2 |
-
APP.api.client = {};
|
| 3 |
-
|
| 4 |
-
APP.api.client.hfDetectAsync = async function (formData) {
|
| 5 |
-
const { state } = APP.core;
|
| 6 |
-
if (!state.hf.baseUrl) return;
|
| 7 |
-
|
| 8 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/async`, {
|
| 9 |
-
method: "POST",
|
| 10 |
-
body: formData
|
| 11 |
-
});
|
| 12 |
-
|
| 13 |
-
if (!resp.ok) {
|
| 14 |
-
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
| 15 |
-
throw new Error(err.detail || "Async detection submission failed");
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
const data = await resp.json();
|
| 19 |
-
|
| 20 |
-
// Store URLs from response
|
| 21 |
-
if (data.status_url) {
|
| 22 |
-
state.hf.statusUrl = data.status_url.startsWith("http")
|
| 23 |
-
? data.status_url
|
| 24 |
-
: `${state.hf.baseUrl}${data.status_url}`;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
if (data.video_url) {
|
| 28 |
-
state.hf.videoUrl = data.video_url.startsWith("http")
|
| 29 |
-
? data.video_url
|
| 30 |
-
: `${state.hf.baseUrl}${data.video_url}`;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
if (data.depth_video_url) {
|
| 34 |
-
state.hf.depthVideoUrl = data.depth_video_url.startsWith("http")
|
| 35 |
-
? data.depth_video_url
|
| 36 |
-
: `${state.hf.baseUrl}${data.depth_video_url}`;
|
| 37 |
-
}
|
| 38 |
-
|
| 39 |
-
if (data.depth_first_frame_url) {
|
| 40 |
-
state.hf.depthFirstFrameUrl = data.depth_first_frame_url.startsWith("http")
|
| 41 |
-
? data.depth_first_frame_url
|
| 42 |
-
: `${state.hf.baseUrl}${data.depth_first_frame_url}`;
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
return data;
|
| 46 |
-
};
|
| 47 |
-
|
| 48 |
-
APP.api.client.checkJobStatus = async function (jobId) {
|
| 49 |
-
const { state } = APP.core;
|
| 50 |
-
if (!state.hf.baseUrl) return { status: "error" };
|
| 51 |
-
|
| 52 |
-
const url = state.hf.statusUrl || `${state.hf.baseUrl}/detect/job/${jobId}`;
|
| 53 |
-
const resp = await fetch(url, { cache: "no-store" });
|
| 54 |
-
|
| 55 |
-
if (!resp.ok) {
|
| 56 |
-
if (resp.status === 404) return { status: "not_found" };
|
| 57 |
-
throw new Error(`Status check failed: ${resp.status}`);
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
return await resp.json();
|
| 61 |
-
};
|
| 62 |
-
|
| 63 |
-
APP.api.client.cancelBackendJob = async function (jobId, reason) {
|
| 64 |
-
const { state } = APP.core;
|
| 65 |
-
const { log } = APP.ui.logging;
|
| 66 |
-
|
| 67 |
-
if (!state.hf.baseUrl || !jobId) return;
|
| 68 |
-
|
| 69 |
-
// Don't attempt cancel on HF Space (it doesn't support it)
|
| 70 |
-
if (state.hf.baseUrl.includes("hf.space")) {
|
| 71 |
-
log(`Job cancel skipped for HF Space (${reason || "user request"})`, "w");
|
| 72 |
-
return { status: "skipped", message: "Cancel disabled for HF Space" };
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
try {
|
| 76 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/job/${jobId}`, {
|
| 77 |
-
method: "DELETE"
|
| 78 |
-
});
|
| 79 |
-
|
| 80 |
-
if (resp.ok) {
|
| 81 |
-
const result = await resp.json();
|
| 82 |
-
log(`Job ${jobId.substring(0, 8)} cancelled`, "w");
|
| 83 |
-
return result;
|
| 84 |
-
}
|
| 85 |
-
if (resp.status === 404) return { status: "not_found" };
|
| 86 |
-
throw new Error("Cancel failed");
|
| 87 |
-
} catch (err) {
|
| 88 |
-
log(`Cancel error: ${err.message}`, "e");
|
| 89 |
-
return { status: "error", message: err.message };
|
| 90 |
-
}
|
| 91 |
-
};
|
| 92 |
-
|
| 93 |
-
/**
|
| 94 |
-
* Sync GPT enrichment data from polled first_frame_detections into state.detections.
|
| 95 |
-
* Returns true if any card was updated and needs re-render.
|
| 96 |
-
*/
|
| 97 |
-
APP.api.client._syncGptFromDetections = function (rawDets, logLabel) {
|
| 98 |
-
const { state } = APP.core;
|
| 99 |
-
const { log } = APP.ui.logging;
|
| 100 |
-
let needsRender = false;
|
| 101 |
-
|
| 102 |
-
// Phase A: Sync assessment status, relevance fields
|
| 103 |
-
for (const rd of rawDets) {
|
| 104 |
-
const tid = rd.track_id || `T${String(rawDets.indexOf(rd) + 1).padStart(2, "0")}`;
|
| 105 |
-
const existing = (state.detections || []).find(d => d.id === tid);
|
| 106 |
-
if (existing) {
|
| 107 |
-
if (rd.assessment_status && existing.assessment_status !== rd.assessment_status) {
|
| 108 |
-
existing.assessment_status = rd.assessment_status;
|
| 109 |
-
needsRender = true;
|
| 110 |
-
}
|
| 111 |
-
if (rd.mission_relevant !== undefined && rd.mission_relevant !== null) {
|
| 112 |
-
existing.mission_relevant = rd.mission_relevant;
|
| 113 |
-
}
|
| 114 |
-
if (rd.relevance_reason) {
|
| 115 |
-
existing.relevance_reason = rd.relevance_reason;
|
| 116 |
-
}
|
| 117 |
-
}
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// Phase B: Full GPT feature merge (gated on gpt_raw)
|
| 121 |
-
const hasGptData = rawDets.some(d => d.gpt_raw);
|
| 122 |
-
if (hasGptData) {
|
| 123 |
-
state.hf.firstFrameDetections = rawDets;
|
| 124 |
-
for (const rd of rawDets) {
|
| 125 |
-
const tid = rd.track_id || `T${String(rawDets.indexOf(rd) + 1).padStart(2, "0")}`;
|
| 126 |
-
const existing = (state.detections || []).find(d => d.id === tid);
|
| 127 |
-
if (existing && rd.gpt_raw) {
|
| 128 |
-
const g = rd.gpt_raw;
|
| 129 |
-
existing.features = APP.core.gptMapping.buildFeatures(g);
|
| 130 |
-
existing.threat_level_score = rd.threat_level_score || g.threat_level_score || 0;
|
| 131 |
-
existing.threat_classification = rd.threat_classification || g.threat_classification || "Unknown";
|
| 132 |
-
existing.weapon_readiness = rd.weapon_readiness || g.weapon_readiness || "Unknown";
|
| 133 |
-
existing.gpt_distance_m = rd.gpt_distance_m || null;
|
| 134 |
-
existing.gpt_direction = rd.gpt_direction || null;
|
| 135 |
-
needsRender = true;
|
| 136 |
-
}
|
| 137 |
-
}
|
| 138 |
-
log(`Track cards updated with GPT assessment${logLabel ? " (" + logLabel + ")" : ""}`, "g");
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
if (needsRender && APP.ui && APP.ui.cards && APP.ui.cards.renderFrameTrackList) {
|
| 142 |
-
APP.ui.cards.renderFrameTrackList();
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
return needsRender;
|
| 146 |
-
};
|
| 147 |
-
|
| 148 |
-
APP.api.client.pollAsyncJob = async function () {
|
| 149 |
-
const { state } = APP.core;
|
| 150 |
-
const { log, setHfStatus } = APP.ui.logging;
|
| 151 |
-
const { fetchProcessedVideo, fetchDepthVideo, fetchDepthFirstFrame } = APP.core.video;
|
| 152 |
-
const syncGpt = APP.api.client._syncGptFromDetections;
|
| 153 |
-
|
| 154 |
-
const pollInterval = 3000; // 3 seconds
|
| 155 |
-
const maxAttempts = 200; // 10 minutes max
|
| 156 |
-
let attempts = 0;
|
| 157 |
-
let fetchingVideo = false;
|
| 158 |
-
|
| 159 |
-
return new Promise((resolve, reject) => {
|
| 160 |
-
state.hf.asyncPollInterval = setInterval(async () => {
|
| 161 |
-
attempts++;
|
| 162 |
-
|
| 163 |
-
try {
|
| 164 |
-
const resp = await fetch(state.hf.statusUrl, { cache: "no-store" });
|
| 165 |
-
|
| 166 |
-
if (!resp.ok) {
|
| 167 |
-
if (resp.status === 404) {
|
| 168 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 169 |
-
reject(new Error("Job expired or not found"));
|
| 170 |
-
return;
|
| 171 |
-
}
|
| 172 |
-
throw new Error(`Status check failed: ${resp.statusText}`);
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
const status = await resp.json();
|
| 176 |
-
state.hf.asyncStatus = status.status;
|
| 177 |
-
state.hf.asyncProgress = status;
|
| 178 |
-
|
| 179 |
-
if (status.status === "completed") {
|
| 180 |
-
if (fetchingVideo) return;
|
| 181 |
-
fetchingVideo = true;
|
| 182 |
-
|
| 183 |
-
const completedJobId = state.hf.asyncJobId;
|
| 184 |
-
log(`✓ Backend job ${completedJobId.substring(0, 8)}: completed successfully`, "g");
|
| 185 |
-
setHfStatus("job completed, fetching video...");
|
| 186 |
-
|
| 187 |
-
// Final GPT sync — enrichment may have completed during
|
| 188 |
-
// processing but the poll never landed on a "processing"
|
| 189 |
-
// cycle that picked it up (common for segmentation mode
|
| 190 |
-
// where _enrich_first_frame_gpt is skipped).
|
| 191 |
-
if (status.first_frame_detections && status.first_frame_detections.length > 0) {
|
| 192 |
-
syncGpt(status.first_frame_detections, "final sync");
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
-
try {
|
| 196 |
-
await fetchProcessedVideo();
|
| 197 |
-
await fetchDepthVideo();
|
| 198 |
-
await fetchDepthFirstFrame();
|
| 199 |
-
|
| 200 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 201 |
-
state.hf.completedJobId = state.hf.asyncJobId; // preserve for post-completion sync
|
| 202 |
-
state.hf.asyncJobId = null;
|
| 203 |
-
setHfStatus("ready");
|
| 204 |
-
resolve();
|
| 205 |
-
} catch (err) {
|
| 206 |
-
if (err && err.code === "VIDEO_PENDING") {
|
| 207 |
-
setHfStatus("job completed, finalizing video...");
|
| 208 |
-
fetchingVideo = false;
|
| 209 |
-
return;
|
| 210 |
-
}
|
| 211 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 212 |
-
state.hf.asyncJobId = null;
|
| 213 |
-
reject(err);
|
| 214 |
-
}
|
| 215 |
-
} else if (status.status === "failed") {
|
| 216 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 217 |
-
const errMsg = status.error || "Processing failed";
|
| 218 |
-
log(`✗ Backend job ${state.hf.asyncJobId.substring(0, 8)}: failed - ${errMsg}`, "e");
|
| 219 |
-
state.hf.asyncJobId = null;
|
| 220 |
-
setHfStatus(`error: ${errMsg}`);
|
| 221 |
-
reject(new Error(errMsg));
|
| 222 |
-
} else {
|
| 223 |
-
// Still processing
|
| 224 |
-
const progressInfo = status.progress ? ` (${Math.round(status.progress * 100)}%)` : "";
|
| 225 |
-
setHfStatus(`job ${state.hf.asyncJobId.substring(0, 8)}: ${status.status}${progressInfo} (${attempts})`);
|
| 226 |
-
|
| 227 |
-
// Check if GPT enrichment has updated first-frame detections
|
| 228 |
-
if (status.first_frame_detections && status.first_frame_detections.length > 0) {
|
| 229 |
-
syncGpt(status.first_frame_detections);
|
| 230 |
-
}
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
if (attempts >= maxAttempts) {
|
| 234 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 235 |
-
reject(new Error("Polling timeout (10 minutes)"));
|
| 236 |
-
}
|
| 237 |
-
} catch (err) {
|
| 238 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 239 |
-
reject(err);
|
| 240 |
-
}
|
| 241 |
-
}, pollInterval);
|
| 242 |
-
});
|
| 243 |
-
};
|
| 244 |
-
|
| 245 |
-
// External detection hook (can be replaced by user)
|
| 246 |
-
APP.api.client.externalDetect = async function (input) {
|
| 247 |
-
console.log("externalDetect called", input);
|
| 248 |
-
return [];
|
| 249 |
-
};
|
| 250 |
-
|
| 251 |
-
// External features hook (can be replaced by user)
|
| 252 |
-
APP.api.client.externalFeatures = async function (detections, frameInfo) {
|
| 253 |
-
console.log("externalFeatures called for", detections.length, "objects");
|
| 254 |
-
return {};
|
| 255 |
-
};
|
| 256 |
-
|
| 257 |
-
// External tracker hook (can be replaced by user)
|
| 258 |
-
APP.api.client.externalTrack = async function (videoEl) {
|
| 259 |
-
console.log("externalTrack called");
|
| 260 |
-
return [];
|
| 261 |
-
};
|
| 262 |
-
|
| 263 |
-
// Call HF object detection directly (for first frame)
|
| 264 |
-
APP.api.client.callHfObjectDetection = async function (canvas) {
|
| 265 |
-
const { state } = APP.core;
|
| 266 |
-
const { canvasToBlob } = APP.core.utils;
|
| 267 |
-
const { CONFIG } = APP.core;
|
| 268 |
-
|
| 269 |
-
const proxyBase = (CONFIG.PROXY_URL || "").trim();
|
| 270 |
-
|
| 271 |
-
if (proxyBase) {
|
| 272 |
-
const blob = await canvasToBlob(canvas);
|
| 273 |
-
const form = new FormData();
|
| 274 |
-
form.append("image", blob, "frame.jpg");
|
| 275 |
-
|
| 276 |
-
const resp = await fetch(`${proxyBase.replace(/\/$/, "")}/detect`, {
|
| 277 |
-
method: "POST",
|
| 278 |
-
body: form
|
| 279 |
-
});
|
| 280 |
-
|
| 281 |
-
if (!resp.ok) {
|
| 282 |
-
let detail = `Proxy inference failed (${resp.status})`;
|
| 283 |
-
try {
|
| 284 |
-
const err = await resp.json();
|
| 285 |
-
detail = err.detail || err.error || detail;
|
| 286 |
-
} catch (_) { }
|
| 287 |
-
throw new Error(detail);
|
| 288 |
-
}
|
| 289 |
-
|
| 290 |
-
return await resp.json();
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
// Default: use the backend base URL
|
| 294 |
-
const blob = await canvasToBlob(canvas);
|
| 295 |
-
const form = new FormData();
|
| 296 |
-
form.append("image", blob, "frame.jpg");
|
| 297 |
-
|
| 298 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/frame`, {
|
| 299 |
-
method: "POST",
|
| 300 |
-
body: form
|
| 301 |
-
});
|
| 302 |
-
|
| 303 |
-
if (!resp.ok) {
|
| 304 |
-
throw new Error(`Frame detection failed: ${resp.statusText}`);
|
| 305 |
-
}
|
| 306 |
-
|
| 307 |
-
return await resp.json();
|
| 308 |
-
};
|
| 309 |
-
|
| 310 |
-
// Capture current video frame and send to backend for GPT analysis
|
| 311 |
-
APP.api.client.analyzeFrame = async function (videoEl, tracks) {
|
| 312 |
-
const { state } = APP.core;
|
| 313 |
-
const { canvasToBlob } = APP.core.utils;
|
| 314 |
-
|
| 315 |
-
// Capture current video frame
|
| 316 |
-
const canvas = document.createElement("canvas");
|
| 317 |
-
canvas.width = videoEl.videoWidth;
|
| 318 |
-
canvas.height = videoEl.videoHeight;
|
| 319 |
-
canvas.getContext("2d").drawImage(videoEl, 0, 0);
|
| 320 |
-
const blob = await canvasToBlob(canvas);
|
| 321 |
-
|
| 322 |
-
// Convert normalized bbox (0-1) back to pixel coords for backend
|
| 323 |
-
const w = canvas.width, h = canvas.height;
|
| 324 |
-
const dets = tracks.map(t => ({
|
| 325 |
-
track_id: t.id,
|
| 326 |
-
label: t.label,
|
| 327 |
-
bbox: [
|
| 328 |
-
Math.round(t.bbox.x * w),
|
| 329 |
-
Math.round(t.bbox.y * h),
|
| 330 |
-
Math.round((t.bbox.x + t.bbox.w) * w),
|
| 331 |
-
Math.round((t.bbox.y + t.bbox.h) * h),
|
| 332 |
-
],
|
| 333 |
-
score: t.score,
|
| 334 |
-
}));
|
| 335 |
-
|
| 336 |
-
const form = new FormData();
|
| 337 |
-
form.append("image", blob, "frame.jpg");
|
| 338 |
-
form.append("detections", JSON.stringify(dets));
|
| 339 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 340 |
-
if (jobId) form.append("job_id", jobId);
|
| 341 |
-
|
| 342 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/analyze-frame`, {
|
| 343 |
-
method: "POST",
|
| 344 |
-
body: form,
|
| 345 |
-
});
|
| 346 |
-
if (!resp.ok) throw new Error(`Frame analysis failed: ${resp.statusText}`);
|
| 347 |
-
return await resp.json();
|
| 348 |
-
};
|
| 349 |
-
|
| 350 |
-
// Chat about threats using GPT
|
| 351 |
-
APP.api.client.chatAboutThreats = async function (question, detections) {
|
| 352 |
-
const { state } = APP.core;
|
| 353 |
-
|
| 354 |
-
const form = new FormData();
|
| 355 |
-
form.append("question", question);
|
| 356 |
-
form.append("detections", JSON.stringify(detections));
|
| 357 |
-
if (state.hf.missionSpec) {
|
| 358 |
-
form.append("mission_context", JSON.stringify(state.hf.missionSpec));
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
const resp = await fetch(`${state.hf.baseUrl}/chat/threat`, {
|
| 362 |
-
method: "POST",
|
| 363 |
-
body: form
|
| 364 |
-
});
|
| 365 |
-
|
| 366 |
-
if (!resp.ok) {
|
| 367 |
-
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
| 368 |
-
throw new Error(err.detail || "Chat request failed");
|
| 369 |
-
}
|
| 370 |
-
|
| 371 |
-
return await resp.json();
|
| 372 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/config.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
APP.core.CONFIG = {
|
| 2 |
-
// API Endpoints will be loaded from window.API_CONFIG or defaults
|
| 3 |
-
BACKEND_BASE: (window.API_CONFIG?.BACKEND_BASE || window.API_CONFIG?.BASE_URL || "").replace(/\/$/, "") || window.location.origin,
|
| 4 |
-
HF_TOKEN: window.API_CONFIG?.HF_TOKEN || "",
|
| 5 |
-
PROXY_URL: (window.API_CONFIG?.PROXY_URL || "").trim(),
|
| 6 |
-
|
| 7 |
-
// Tracking Constants
|
| 8 |
-
REASON_INTERVAL: 30,
|
| 9 |
-
MAX_TRACKS: 50,
|
| 10 |
-
TRACK_PRUNE_MS: 1500,
|
| 11 |
-
TRACK_MATCH_THRESHOLD: 0.25,
|
| 12 |
-
|
| 13 |
-
// Default Queries
|
| 14 |
-
DEFAULT_QUERY_CLASSES: ["drone", "uav", "quadcopter", "fixed-wing", "missile", "person", "vehicle"]
|
| 15 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/demo.js
DELETED
|
@@ -1,142 +0,0 @@
|
|
| 1 |
-
APP.core.demo = {};
|
| 2 |
-
|
| 3 |
-
APP.core.demo.data = null;
|
| 4 |
-
APP.core.demo.active = false;
|
| 5 |
-
|
| 6 |
-
APP.core.demo.load = async function () {
|
| 7 |
-
// Demo data is now loaded per-video via loadForVideo()
|
| 8 |
-
// This function is kept for compatibility but does nothing
|
| 9 |
-
};
|
| 10 |
-
|
| 11 |
-
APP.core.demo.getFrameData = function (currentTime) {
|
| 12 |
-
if (!APP.core.demo.data) return null;
|
| 13 |
-
|
| 14 |
-
// Use interpolation for keyframe format
|
| 15 |
-
if (APP.core.demo.data.format === "keyframes") {
|
| 16 |
-
return APP.core.demo.getFrameDataInterpolated(currentTime);
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
// Original logic for frame-by-frame data
|
| 20 |
-
const fps = APP.core.demo.data.fps || 30;
|
| 21 |
-
const frameIdx = Math.floor(currentTime * fps);
|
| 22 |
-
|
| 23 |
-
// Get tracks for this frame
|
| 24 |
-
// Handle both string and number keys if needed
|
| 25 |
-
const tracks = APP.core.demo.data.frames[frameIdx] || APP.core.demo.data.frames[frameIdx.toString()];
|
| 26 |
-
|
| 27 |
-
if (!tracks) return [];
|
| 28 |
-
return tracks;
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
APP.core.demo.enable = function (force = true) {
|
| 32 |
-
const { log } = APP.ui.logging;
|
| 33 |
-
APP.core.demo.active = force;
|
| 34 |
-
if (force) {
|
| 35 |
-
log("DEMO MODE ACTIVATED", "g");
|
| 36 |
-
const chipFeed = document.getElementById("chipFeed");
|
| 37 |
-
if (chipFeed) chipFeed.textContent = "MODE:DEMO";
|
| 38 |
-
}
|
| 39 |
-
};
|
| 40 |
-
|
| 41 |
-
// Keyframe interpolation for smooth radar movement
|
| 42 |
-
APP.core.demo._keyframeIndices = null;
|
| 43 |
-
|
| 44 |
-
APP.core.demo.getKeyframeIndices = function() {
|
| 45 |
-
if (!APP.core.demo.data || APP.core.demo.data.format !== "keyframes") return null;
|
| 46 |
-
if (APP.core.demo._keyframeIndices) return APP.core.demo._keyframeIndices;
|
| 47 |
-
|
| 48 |
-
const indices = Object.keys(APP.core.demo.data.keyframes)
|
| 49 |
-
.map(k => parseInt(k, 10))
|
| 50 |
-
.sort((a, b) => a - b);
|
| 51 |
-
|
| 52 |
-
APP.core.demo._keyframeIndices = indices;
|
| 53 |
-
return indices;
|
| 54 |
-
};
|
| 55 |
-
|
| 56 |
-
APP.core.demo.interpolateTrack = function(trackA, trackB, t) {
|
| 57 |
-
const { lerp } = APP.core.utils;
|
| 58 |
-
|
| 59 |
-
return {
|
| 60 |
-
id: trackA.id,
|
| 61 |
-
label: trackA.label,
|
| 62 |
-
bbox: {
|
| 63 |
-
x: lerp(trackA.bbox.x, trackB.bbox.x, t),
|
| 64 |
-
y: lerp(trackA.bbox.y, trackB.bbox.y, t),
|
| 65 |
-
w: lerp(trackA.bbox.w, trackB.bbox.w, t),
|
| 66 |
-
h: lerp(trackA.bbox.h, trackB.bbox.h, t)
|
| 67 |
-
},
|
| 68 |
-
gpt_distance_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t),
|
| 69 |
-
angle_deg: trackA.angle_deg,
|
| 70 |
-
speed_kph: lerp(trackA.speed_kph, trackB.speed_kph, t),
|
| 71 |
-
depth_valid: true,
|
| 72 |
-
depth_est_m: lerp(trackA.gpt_distance_m, trackB.gpt_distance_m, t),
|
| 73 |
-
history: [],
|
| 74 |
-
predicted_path: []
|
| 75 |
-
};
|
| 76 |
-
};
|
| 77 |
-
|
| 78 |
-
APP.core.demo.getFrameDataInterpolated = function(currentTime) {
|
| 79 |
-
const data = APP.core.demo.data;
|
| 80 |
-
if (!data || data.format !== "keyframes") return null;
|
| 81 |
-
|
| 82 |
-
const fps = data.fps || 24;
|
| 83 |
-
const frameIdx = Math.floor(currentTime * fps);
|
| 84 |
-
const keyframes = APP.core.demo.getKeyframeIndices();
|
| 85 |
-
|
| 86 |
-
if (!keyframes || keyframes.length === 0) return [];
|
| 87 |
-
|
| 88 |
-
// Find surrounding keyframes
|
| 89 |
-
let beforeIdx = keyframes[0];
|
| 90 |
-
let afterIdx = keyframes[keyframes.length - 1];
|
| 91 |
-
|
| 92 |
-
for (let i = 0; i < keyframes.length; i++) {
|
| 93 |
-
if (keyframes[i] <= frameIdx) beforeIdx = keyframes[i];
|
| 94 |
-
if (keyframes[i] >= frameIdx) { afterIdx = keyframes[i]; break; }
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
// Edge cases
|
| 98 |
-
if (frameIdx <= keyframes[0]) return data.keyframes[keyframes[0]] || [];
|
| 99 |
-
if (frameIdx >= keyframes[keyframes.length - 1]) return data.keyframes[keyframes[keyframes.length - 1]] || [];
|
| 100 |
-
|
| 101 |
-
// Interpolation factor
|
| 102 |
-
const t = (beforeIdx === afterIdx) ? 0 : (frameIdx - beforeIdx) / (afterIdx - beforeIdx);
|
| 103 |
-
|
| 104 |
-
const tracksBefore = data.keyframes[beforeIdx] || [];
|
| 105 |
-
const tracksAfter = data.keyframes[afterIdx] || [];
|
| 106 |
-
|
| 107 |
-
// Match by ID and interpolate
|
| 108 |
-
const result = [];
|
| 109 |
-
for (const trackA of tracksBefore) {
|
| 110 |
-
const trackB = tracksAfter.find(tr => tr.id === trackA.id);
|
| 111 |
-
if (trackB) {
|
| 112 |
-
result.push(APP.core.demo.interpolateTrack(trackA, trackB, t));
|
| 113 |
-
}
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
return result;
|
| 117 |
-
};
|
| 118 |
-
|
| 119 |
-
// Video-specific loading for demo tracks
|
| 120 |
-
APP.core.demo.loadForVideo = async function(videoName) {
|
| 121 |
-
const { log } = APP.ui.logging;
|
| 122 |
-
if (videoName.toLowerCase().includes("enhance_video_movement")) {
|
| 123 |
-
// Use global variable injected by helicopter_demo_data.js script tag (CORS-safe)
|
| 124 |
-
if (window.HELICOPTER_DEMO_DATA) {
|
| 125 |
-
APP.core.demo.data = window.HELICOPTER_DEMO_DATA;
|
| 126 |
-
APP.core.demo._keyframeIndices = null; // Reset cache
|
| 127 |
-
log("Helicopter demo tracks loaded (CORS-safe mode).", "g");
|
| 128 |
-
return;
|
| 129 |
-
}
|
| 130 |
-
// Fallback to fetch (works when served from HTTP server)
|
| 131 |
-
try {
|
| 132 |
-
const resp = await fetch("data/helicopter_demo_tracks.json");
|
| 133 |
-
if (resp.ok) {
|
| 134 |
-
APP.core.demo.data = await resp.json();
|
| 135 |
-
APP.core.demo._keyframeIndices = null; // Reset cache
|
| 136 |
-
log("Helicopter demo tracks loaded.", "g");
|
| 137 |
-
}
|
| 138 |
-
} catch (err) {
|
| 139 |
-
console.warn("Failed to load helicopter demo tracks:", err);
|
| 140 |
-
}
|
| 141 |
-
}
|
| 142 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/gptMapping.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* gptMapping.js — canonical GPT-raw → features field mapping.
|
| 3 |
-
*
|
| 4 |
-
* Replaces 4 identical inline mapping blocks across main.js, client.js,
|
| 5 |
-
* and tracker.js (2 locations).
|
| 6 |
-
*/
|
| 7 |
-
APP.core.gptMapping = {};
|
| 8 |
-
|
| 9 |
-
/** Frozen assessment-status string constants. */
|
| 10 |
-
APP.core.gptMapping.STATUS = Object.freeze({
|
| 11 |
-
ASSESSED: "ASSESSED",
|
| 12 |
-
UNASSESSED: "UNASSESSED",
|
| 13 |
-
STALE: "STALE",
|
| 14 |
-
PENDING_GPT: "PENDING_GPT",
|
| 15 |
-
});
|
| 16 |
-
|
| 17 |
-
/**
|
| 18 |
-
* Build a features object from a gpt_raw payload.
|
| 19 |
-
*
|
| 20 |
-
* @param {Object|null|undefined} gptRaw - The gpt_raw dict from a detection.
|
| 21 |
-
* @returns {Object} Features key-value map (empty object if gptRaw is falsy).
|
| 22 |
-
*/
|
| 23 |
-
APP.core.gptMapping.buildFeatures = function (gptRaw) {
|
| 24 |
-
if (!gptRaw) return {};
|
| 25 |
-
const rangeStr = gptRaw.range_estimate && gptRaw.range_estimate !== "Unknown"
|
| 26 |
-
? gptRaw.range_estimate + " (est.)" : "Unknown";
|
| 27 |
-
const features = {
|
| 28 |
-
"Type": gptRaw.object_type || "Unknown",
|
| 29 |
-
"Size": gptRaw.size || "Unknown",
|
| 30 |
-
"Threat Lvl": (gptRaw.threat_level || gptRaw.threat_level_score || "?") + "/10",
|
| 31 |
-
"Status": gptRaw.threat_classification || "?",
|
| 32 |
-
"Weapons": (gptRaw.visible_weapons || []).join(", ") || "None Visible",
|
| 33 |
-
"Readiness": gptRaw.weapon_readiness || "Unknown",
|
| 34 |
-
"Motion": gptRaw.motion_status || "Unknown",
|
| 35 |
-
"Range": rangeStr,
|
| 36 |
-
"Bearing": gptRaw.bearing || "Unknown",
|
| 37 |
-
"Intent": gptRaw.tactical_intent || "Unknown",
|
| 38 |
-
};
|
| 39 |
-
const dynFeats = gptRaw.dynamic_features || [];
|
| 40 |
-
for (const feat of dynFeats) {
|
| 41 |
-
if (feat && feat.key && feat.value) {
|
| 42 |
-
features[feat.key] = feat.value;
|
| 43 |
-
}
|
| 44 |
-
}
|
| 45 |
-
return features;
|
| 46 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/hel.js
DELETED
|
@@ -1,245 +0,0 @@
|
|
| 1 |
-
// HEL (High-Energy Laser) Physics and Computation Module
|
| 2 |
-
APP.core.hel = {};
|
| 3 |
-
|
| 4 |
-
// Get current knob values from UI
|
| 5 |
-
APP.core.hel.getKnobs = function () {
|
| 6 |
-
const { $ } = APP.core.utils;
|
| 7 |
-
|
| 8 |
-
const helPower = $("#helPower");
|
| 9 |
-
const helAperture = $("#helAperture");
|
| 10 |
-
const helM2 = $("#helM2");
|
| 11 |
-
const helJitter = $("#helJitter");
|
| 12 |
-
const helDuty = $("#helDuty");
|
| 13 |
-
const helMode = $("#helMode");
|
| 14 |
-
const atmVis = $("#atmVis");
|
| 15 |
-
const atmCn2 = $("#atmCn2");
|
| 16 |
-
const seaSpray = $("#seaSpray");
|
| 17 |
-
const aoQ = $("#aoQ");
|
| 18 |
-
const rangeBase = $("#rangeBase");
|
| 19 |
-
|
| 20 |
-
if (!helPower) return {};
|
| 21 |
-
|
| 22 |
-
return {
|
| 23 |
-
PkW: +helPower.value,
|
| 24 |
-
aperture: +helAperture.value,
|
| 25 |
-
M2: +helM2.value,
|
| 26 |
-
jitter_urad: +helJitter.value,
|
| 27 |
-
duty: (+helDuty.value) / 100,
|
| 28 |
-
mode: helMode ? helMode.value : "cw",
|
| 29 |
-
vis_km: +atmVis.value,
|
| 30 |
-
cn2: +atmCn2.value,
|
| 31 |
-
spray: +seaSpray.value,
|
| 32 |
-
ao: +aoQ.value,
|
| 33 |
-
baseRange: +rangeBase.value
|
| 34 |
-
};
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
// Compute max power at target considering atmospheric effects
|
| 38 |
-
APP.core.hel.maxPowerAtTarget = function (range_m) {
|
| 39 |
-
const k = APP.core.hel.getKnobs();
|
| 40 |
-
if (!k.PkW) return { Ptar: 0, Pout: k.PkW || 0, trans: 0, turb: 0, beam: 0 };
|
| 41 |
-
|
| 42 |
-
const { clamp } = APP.core.utils;
|
| 43 |
-
|
| 44 |
-
// Atmospheric transmission (Beer-Lambert approximation)
|
| 45 |
-
const sigma_km = 3.912 / Math.max(1, k.vis_km);
|
| 46 |
-
const range_km = range_m / 1000;
|
| 47 |
-
const trans = Math.exp(-sigma_km * range_km);
|
| 48 |
-
|
| 49 |
-
// Turbulence factor (simplified Cn² model)
|
| 50 |
-
const cn2_factor = clamp(1 - (k.cn2 / 10) * 0.3, 0.5, 1);
|
| 51 |
-
const ao_correction = 1 + (k.ao / 10) * 0.25;
|
| 52 |
-
const turb = cn2_factor * ao_correction;
|
| 53 |
-
|
| 54 |
-
// Sea spray attenuation
|
| 55 |
-
const spray_factor = 1 - (k.spray / 10) * 0.15;
|
| 56 |
-
|
| 57 |
-
// Beam quality degradation with range
|
| 58 |
-
const beam_spread = 1 + (k.M2 - 1) * (range_km / 5);
|
| 59 |
-
const jitter_loss = 1 / (1 + (k.jitter_urad / 10) * (range_km / 3));
|
| 60 |
-
const beam = jitter_loss / beam_spread;
|
| 61 |
-
|
| 62 |
-
// Total power at target
|
| 63 |
-
const Pout = k.PkW * k.duty;
|
| 64 |
-
const Ptar = Pout * trans * turb * spray_factor * beam;
|
| 65 |
-
|
| 66 |
-
return {
|
| 67 |
-
Ptar: Math.max(0, Ptar),
|
| 68 |
-
Pout,
|
| 69 |
-
trans,
|
| 70 |
-
turb: turb * spray_factor,
|
| 71 |
-
beam
|
| 72 |
-
};
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
// Estimate required power based on target features
|
| 76 |
-
APP.core.hel.requiredPowerFromFeatures = function (feat) {
|
| 77 |
-
if (!feat) return 35; // Default
|
| 78 |
-
|
| 79 |
-
let base = 30; // kW base requirement
|
| 80 |
-
|
| 81 |
-
// Adjust based on material
|
| 82 |
-
const material = (feat.material || "").toLowerCase();
|
| 83 |
-
if (material.includes("metal") || material.includes("aluminum")) base *= 1.2;
|
| 84 |
-
if (material.includes("composite") || material.includes("carbon")) base *= 0.8;
|
| 85 |
-
if (material.includes("plastic") || material.includes("polymer")) base *= 0.6;
|
| 86 |
-
|
| 87 |
-
// Adjust based on reflectivity
|
| 88 |
-
const refl = feat.reflectivity;
|
| 89 |
-
if (typeof refl === "number") {
|
| 90 |
-
base *= (1 + refl * 0.5); // Higher reflectivity = more power needed
|
| 91 |
-
}
|
| 92 |
-
|
| 93 |
-
// Adjust based on size
|
| 94 |
-
const size = feat.physical_size;
|
| 95 |
-
if (typeof size === "number") {
|
| 96 |
-
base *= Math.max(0.5, Math.min(2, size / 2)); // Assuming size in meters
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
return Math.round(base);
|
| 100 |
-
};
|
| 101 |
-
|
| 102 |
-
// Calculate required dwell time
|
| 103 |
-
APP.core.hel.requiredDwell = function (range_m, reqP_kW, maxP_kW, baseDwell_s) {
|
| 104 |
-
if (maxP_kW <= 0) return Infinity;
|
| 105 |
-
if (maxP_kW >= reqP_kW) return baseDwell_s;
|
| 106 |
-
|
| 107 |
-
// Dwell inflates quadratically as power drops below requirement
|
| 108 |
-
const ratio = reqP_kW / maxP_kW;
|
| 109 |
-
return baseDwell_s * ratio * ratio;
|
| 110 |
-
};
|
| 111 |
-
|
| 112 |
-
// Calculate probability of kill
|
| 113 |
-
APP.core.hel.pkillFromMargin = function (margin_kW, baseDwell_s, reqDwell_s) {
|
| 114 |
-
if (margin_kW <= 0) return 0;
|
| 115 |
-
if (reqDwell_s <= 0) return 0;
|
| 116 |
-
|
| 117 |
-
const dwellRatio = baseDwell_s / reqDwell_s;
|
| 118 |
-
const marginFactor = Math.min(1, margin_kW / 50); // Normalize margin
|
| 119 |
-
|
| 120 |
-
return Math.min(0.99, dwellRatio * marginFactor * 0.95);
|
| 121 |
-
};
|
| 122 |
-
|
| 123 |
-
// Recompute HEL synthesis for all detections
|
| 124 |
-
APP.core.hel.recomputeHEL = async function () {
|
| 125 |
-
const { state } = APP.core;
|
| 126 |
-
const { log } = APP.ui.logging;
|
| 127 |
-
const knobs = APP.core.hel.getKnobs();
|
| 128 |
-
|
| 129 |
-
if (!state.detections || state.detections.length === 0) return;
|
| 130 |
-
|
| 131 |
-
for (const det of state.detections) {
|
| 132 |
-
// Get range from GPT or use baseline
|
| 133 |
-
const range = det.gpt_distance_m || knobs.baseRange || 1500;
|
| 134 |
-
|
| 135 |
-
// Compute power at target
|
| 136 |
-
const power = APP.core.hel.maxPowerAtTarget(range);
|
| 137 |
-
det.maxP_kW = power.Ptar;
|
| 138 |
-
|
| 139 |
-
// Required power from features
|
| 140 |
-
det.reqP_kW = APP.core.hel.requiredPowerFromFeatures(det.features);
|
| 141 |
-
|
| 142 |
-
// Dwell calculation
|
| 143 |
-
det.baseDwell_s = det.baseDwell_s || 5.0;
|
| 144 |
-
det.reqDwell_s = APP.core.hel.requiredDwell(range, det.reqP_kW, det.maxP_kW, det.baseDwell_s);
|
| 145 |
-
|
| 146 |
-
// P(kill)
|
| 147 |
-
const margin = det.maxP_kW - det.reqP_kW;
|
| 148 |
-
det.pkill = APP.core.hel.pkillFromMargin(margin, det.baseDwell_s, det.reqDwell_s);
|
| 149 |
-
|
| 150 |
-
// Store range
|
| 151 |
-
det.baseRange_m = range;
|
| 152 |
-
}
|
| 153 |
-
|
| 154 |
-
log("HEL synthesis updated for all targets.", "t");
|
| 155 |
-
};
|
| 156 |
-
|
| 157 |
-
// External hook for HEL synthesis (can be replaced by user)
|
| 158 |
-
APP.core.hel.externalHEL = async function (detections, knobs) {
|
| 159 |
-
// Default implementation - can be replaced by user
|
| 160 |
-
console.log("externalHEL called for", detections.length, "objects", knobs);
|
| 161 |
-
return {
|
| 162 |
-
targets: {},
|
| 163 |
-
system: { maxP_kW: 0, reqP_kW: 0, margin_kW: 0, medianRange_m: 0 }
|
| 164 |
-
};
|
| 165 |
-
};
|
| 166 |
-
|
| 167 |
-
// Sync knob display values in the UI
|
| 168 |
-
APP.core.hel.syncKnobDisplays = function () {
|
| 169 |
-
const { $ } = APP.core.utils;
|
| 170 |
-
|
| 171 |
-
const helPower = $("#helPower");
|
| 172 |
-
const helAperture = $("#helAperture");
|
| 173 |
-
const helM2 = $("#helM2");
|
| 174 |
-
const helJitter = $("#helJitter");
|
| 175 |
-
const helDuty = $("#helDuty");
|
| 176 |
-
const atmVis = $("#atmVis");
|
| 177 |
-
const atmCn2 = $("#atmCn2");
|
| 178 |
-
const seaSpray = $("#seaSpray");
|
| 179 |
-
const aoQ = $("#aoQ");
|
| 180 |
-
const rangeBase = $("#rangeBase");
|
| 181 |
-
const detHz = $("#detHz");
|
| 182 |
-
const assessWindow = $("#assessWindow");
|
| 183 |
-
const policyMode = $("#policyMode");
|
| 184 |
-
|
| 185 |
-
if (helPower) {
|
| 186 |
-
const v = $("#helPowerVal");
|
| 187 |
-
if (v) v.textContent = helPower.value;
|
| 188 |
-
}
|
| 189 |
-
if (helAperture) {
|
| 190 |
-
const v = $("#helApertureVal");
|
| 191 |
-
if (v) v.textContent = (+helAperture.value).toFixed(2);
|
| 192 |
-
}
|
| 193 |
-
if (helM2) {
|
| 194 |
-
const v = $("#helM2Val");
|
| 195 |
-
if (v) v.textContent = (+helM2.value).toFixed(1);
|
| 196 |
-
}
|
| 197 |
-
if (helJitter) {
|
| 198 |
-
const v = $("#helJitterVal");
|
| 199 |
-
if (v) v.textContent = (+helJitter.value).toFixed(1);
|
| 200 |
-
}
|
| 201 |
-
if (helDuty) {
|
| 202 |
-
const v = $("#helDutyVal");
|
| 203 |
-
if (v) v.textContent = helDuty.value;
|
| 204 |
-
}
|
| 205 |
-
if (atmVis) {
|
| 206 |
-
const v = $("#atmVisVal");
|
| 207 |
-
if (v) v.textContent = atmVis.value;
|
| 208 |
-
}
|
| 209 |
-
if (atmCn2) {
|
| 210 |
-
const v = $("#atmCn2Val");
|
| 211 |
-
if (v) v.textContent = atmCn2.value;
|
| 212 |
-
}
|
| 213 |
-
if (seaSpray) {
|
| 214 |
-
const v = $("#seaSprayVal");
|
| 215 |
-
if (v) v.textContent = seaSpray.value;
|
| 216 |
-
}
|
| 217 |
-
if (aoQ) {
|
| 218 |
-
const v = $("#aoQVal");
|
| 219 |
-
if (v) v.textContent = aoQ.value;
|
| 220 |
-
}
|
| 221 |
-
if (rangeBase) {
|
| 222 |
-
const v = $("#rangeBaseVal");
|
| 223 |
-
if (v) v.textContent = rangeBase.value;
|
| 224 |
-
}
|
| 225 |
-
if (detHz) {
|
| 226 |
-
const v = $("#detHzVal");
|
| 227 |
-
if (v) v.textContent = detHz.value;
|
| 228 |
-
}
|
| 229 |
-
if (assessWindow) {
|
| 230 |
-
const v = $("#assessWindowVal");
|
| 231 |
-
if (v) v.textContent = (+assessWindow.value).toFixed(1);
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
// Update chips
|
| 235 |
-
const chipPolicy = $("#chipPolicy");
|
| 236 |
-
const chipHz = $("#chipHz");
|
| 237 |
-
if (chipPolicy && policyMode) chipPolicy.textContent = `POLICY:${policyMode.value.toUpperCase()}`;
|
| 238 |
-
if (chipHz && detHz) chipHz.textContent = `DET:${detHz.value}Hz`;
|
| 239 |
-
|
| 240 |
-
// Update telemetry footer
|
| 241 |
-
const telemetry = $("#telemetry");
|
| 242 |
-
if (telemetry && helPower && atmVis && atmCn2 && aoQ && detHz) {
|
| 243 |
-
telemetry.textContent = `VIS=${atmVis.value}km · DET=${detHz.value}Hz`;
|
| 244 |
-
}
|
| 245 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/physics.js
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
APP.core.physics = {};
|
| 2 |
-
|
| 3 |
-
APP.core.physics.defaultAimpoint = function (label) {
|
| 4 |
-
const l = (label || "object").toLowerCase();
|
| 5 |
-
if (l.includes("airplane") || l.includes("drone") || l.includes("uav") || l.includes("kite") || l.includes("bird")) {
|
| 6 |
-
return { relx: 0.62, rely: 0.55, label: "engine" };
|
| 7 |
-
}
|
| 8 |
-
if (l.includes("helicopter")) {
|
| 9 |
-
return { relx: 0.50, rely: 0.45, label: "rotor_hub" };
|
| 10 |
-
}
|
| 11 |
-
if (l.includes("boat") || l.includes("ship")) {
|
| 12 |
-
return { relx: 0.60, rely: 0.55, label: "bridge/engine" };
|
| 13 |
-
}
|
| 14 |
-
if (l.includes("truck") || l.includes("car")) {
|
| 15 |
-
return { relx: 0.55, rely: 0.62, label: "engine_block" };
|
| 16 |
-
}
|
| 17 |
-
return { relx: 0.50, rely: 0.55, label: "center_mass" };
|
| 18 |
-
};
|
| 19 |
-
|
| 20 |
-
APP.core.physics.aimpointByLabel = function (label) {
|
| 21 |
-
const l = String(label || "").toLowerCase();
|
| 22 |
-
if (l.includes("engine") || l.includes("fuel")) return { relx: 0.64, rely: 0.58, label: label };
|
| 23 |
-
if (l.includes("wing")) return { relx: 0.42, rely: 0.52, label: label };
|
| 24 |
-
if (l.includes("nose") || l.includes("sensor")) return { relx: 0.28, rely: 0.48, label: label };
|
| 25 |
-
if (l.includes("rotor")) return { relx: 0.52, rely: 0.42, label: label };
|
| 26 |
-
return { relx: 0.50, rely: 0.55, label: label || "center_mass" };
|
| 27 |
-
};
|
| 28 |
-
|
| 29 |
-
APP.core.physics.getKnobs = function () {
|
| 30 |
-
const { $ } = APP.core.utils;
|
| 31 |
-
const helPower = $("#helPower");
|
| 32 |
-
const helAperture = $("#helAperture");
|
| 33 |
-
const helM2 = $("#helM2");
|
| 34 |
-
const helJitter = $("#helJitter");
|
| 35 |
-
const helDuty = $("#helDuty");
|
| 36 |
-
const helMode = $("#helMode");
|
| 37 |
-
const atmVis = $("#atmVis");
|
| 38 |
-
const atmCn2 = $("#atmCn2");
|
| 39 |
-
const seaSpray = $("#seaSpray");
|
| 40 |
-
const aoQ = $("#aoQ");
|
| 41 |
-
const rangeBase = $("#rangeBase");
|
| 42 |
-
|
| 43 |
-
if (!helPower) return {};
|
| 44 |
-
|
| 45 |
-
const PkW = +helPower.value;
|
| 46 |
-
const aperture = +helAperture.value;
|
| 47 |
-
const M2 = +helM2.value;
|
| 48 |
-
const jitter_urad = +helJitter.value;
|
| 49 |
-
const duty = (+helDuty.value) / 100;
|
| 50 |
-
const mode = helMode ? helMode.value : "cw";
|
| 51 |
-
const vis_km = +atmVis.value;
|
| 52 |
-
const cn2 = +atmCn2.value;
|
| 53 |
-
const spray = +seaSpray.value;
|
| 54 |
-
const ao = +aoQ.value;
|
| 55 |
-
const baseRange = +rangeBase.value;
|
| 56 |
-
return { PkW, aperture, M2, jitter_urad, duty, mode, vis_km, cn2, spray, ao, baseRange };
|
| 57 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/state.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
| 1 |
-
APP.core.state = {
|
| 2 |
-
videoUrl: null,
|
| 3 |
-
videoFile: null,
|
| 4 |
-
videoLoaded: false,
|
| 5 |
-
useProcessedFeed: false,
|
| 6 |
-
useDepthFeed: false, // Flag for depth view (Tab 2 video)
|
| 7 |
-
useFrameDepthView: false, // Flag for first frame depth view (Tab 1)
|
| 8 |
-
hasReasoned: false,
|
| 9 |
-
firstFrameReady: false, // Flag for first frame radar display
|
| 10 |
-
isReasoning: false, // Flag to prevent concurrent Reason executions
|
| 11 |
-
|
| 12 |
-
hf: {
|
| 13 |
-
// Will be properly initialized after CONFIG loads
|
| 14 |
-
baseUrl: (window.API_CONFIG?.BACKEND_BASE || window.API_CONFIG?.BASE_URL || "").replace(/\/$/, "") || window.location.origin,
|
| 15 |
-
detector: "auto",
|
| 16 |
-
asyncJobId: null, // Current job ID from /detect/async
|
| 17 |
-
completedJobId: null, // Preserved job ID for post-completion track sync
|
| 18 |
-
asyncPollInterval: null, // Polling timer handle
|
| 19 |
-
firstFrameUrl: null, // First frame preview URL
|
| 20 |
-
firstFrameDetections: null, // First-frame detections from backend
|
| 21 |
-
statusUrl: null, // Status polling URL
|
| 22 |
-
videoUrl: null, // Final video URL
|
| 23 |
-
asyncStatus: "idle", // "idle"|"processing"|"completed"|"failed"
|
| 24 |
-
asyncProgress: null, // Progress data from status endpoint
|
| 25 |
-
queries: [], // Mission objective used as query
|
| 26 |
-
processedUrl: null,
|
| 27 |
-
processedBlob: null,
|
| 28 |
-
depthVideoUrl: null, // Depth video URL
|
| 29 |
-
depthFirstFrameUrl: null, // First frame depth URL
|
| 30 |
-
depthBlob: null, // Depth video blob
|
| 31 |
-
depthFirstFrameBlob: null, // Depth first frame blob
|
| 32 |
-
summary: null,
|
| 33 |
-
busy: false,
|
| 34 |
-
lastError: null,
|
| 35 |
-
missionId: null,
|
| 36 |
-
missionSpec: null,
|
| 37 |
-
plan: null
|
| 38 |
-
},
|
| 39 |
-
|
| 40 |
-
detector: {
|
| 41 |
-
mode: "coco",
|
| 42 |
-
kind: "object",
|
| 43 |
-
loaded: false,
|
| 44 |
-
model: null,
|
| 45 |
-
loading: false,
|
| 46 |
-
cocoBlocked: false,
|
| 47 |
-
hfTrackingWarned: false
|
| 48 |
-
},
|
| 49 |
-
|
| 50 |
-
tracker: {
|
| 51 |
-
mode: "iou",
|
| 52 |
-
tracks: [],
|
| 53 |
-
nextId: 1,
|
| 54 |
-
lastDetTime: 0,
|
| 55 |
-
lastHFSync: 0,
|
| 56 |
-
running: false,
|
| 57 |
-
selectedTrackId: null,
|
| 58 |
-
beamOn: false,
|
| 59 |
-
lastFrameTime: 0,
|
| 60 |
-
frameCount: 0,
|
| 61 |
-
_lastCardRenderFrame: 0, // Frame count at last card render
|
| 62 |
-
_gptBusy: false // Prevent overlapping GPT calls
|
| 63 |
-
},
|
| 64 |
-
|
| 65 |
-
frame: {
|
| 66 |
-
w: 1280,
|
| 67 |
-
h: 720,
|
| 68 |
-
bitmap: null
|
| 69 |
-
},
|
| 70 |
-
|
| 71 |
-
detections: [], // from Tab 1
|
| 72 |
-
selectedId: null,
|
| 73 |
-
|
| 74 |
-
intelBusy: false,
|
| 75 |
-
|
| 76 |
-
ui: {
|
| 77 |
-
cursorMode: "on",
|
| 78 |
-
agentCursor: { x: 0.65, y: 0.28, vx: 0, vy: 0, visible: false, target: null, mode: "idle", t0: 0 }
|
| 79 |
-
}
|
| 80 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/tracker.js
DELETED
|
@@ -1,278 +0,0 @@
|
|
| 1 |
-
APP.core.tracker = {};
|
| 2 |
-
|
| 3 |
-
APP.core.tracker.matchAndUpdateTracks = function (dets, dtSec) {
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { CONFIG } = APP.core;
|
| 6 |
-
const { normBBox, lerp, now, $ } = APP.core.utils;
|
| 7 |
-
const { defaultAimpoint } = APP.core.physics;
|
| 8 |
-
const { log } = APP.ui.logging;
|
| 9 |
-
|
| 10 |
-
const videoEngage = $("#videoEngage");
|
| 11 |
-
const rangeBase = $("#rangeBase"); // Fixed Selector
|
| 12 |
-
|
| 13 |
-
if (!videoEngage) return;
|
| 14 |
-
|
| 15 |
-
// IOU helper
|
| 16 |
-
function iou(a, b) {
|
| 17 |
-
const ax2 = a.x + a.w, ay2 = a.y + a.h;
|
| 18 |
-
const bx2 = b.x + b.w, by2 = b.y + b.h;
|
| 19 |
-
const ix1 = Math.max(a.x, b.x), iy1 = Math.max(a.y, b.y);
|
| 20 |
-
const ix2 = Math.min(ax2, bx2), iy2 = Math.min(ay2, by2);
|
| 21 |
-
const iw = Math.max(0, ix2 - ix1), ih = Math.max(0, iy2 - iy1);
|
| 22 |
-
const inter = iw * ih;
|
| 23 |
-
const ua = a.w * a.h + b.w * b.h - inter;
|
| 24 |
-
return ua <= 0 ? 0 : inter / ua;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
// Convert detections to bbox in video coordinates
|
| 28 |
-
const w = videoEngage.videoWidth || state.frame.w;
|
| 29 |
-
const h = videoEngage.videoHeight || state.frame.h;
|
| 30 |
-
|
| 31 |
-
const detObjs = dets.map(d => ({
|
| 32 |
-
bbox: normBBox(d.bbox, w, h),
|
| 33 |
-
label: d.class,
|
| 34 |
-
score: d.score,
|
| 35 |
-
depth_rel: Number.isFinite(d.depth_rel) ? d.depth_rel : null,
|
| 36 |
-
depth_est_m: d.depth_est_m,
|
| 37 |
-
depth_valid: d.depth_valid
|
| 38 |
-
}));
|
| 39 |
-
|
| 40 |
-
// mark all tracks as unmatched
|
| 41 |
-
const tracks = state.tracker.tracks;
|
| 42 |
-
const used = new Set();
|
| 43 |
-
|
| 44 |
-
for (const tr of tracks) {
|
| 45 |
-
let best = null;
|
| 46 |
-
let bestI = 0.0;
|
| 47 |
-
let bestIdx = -1;
|
| 48 |
-
for (let i = 0; i < detObjs.length; i++) {
|
| 49 |
-
if (used.has(i)) continue;
|
| 50 |
-
const IoU = iou(tr.bbox, detObjs[i].bbox);
|
| 51 |
-
if (IoU > bestI) {
|
| 52 |
-
bestI = IoU;
|
| 53 |
-
best = detObjs[i];
|
| 54 |
-
bestIdx = i;
|
| 55 |
-
}
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
// Strict matching threshold
|
| 59 |
-
if (best && bestI >= CONFIG.TRACK_MATCH_THRESHOLD) {
|
| 60 |
-
used.add(bestIdx);
|
| 61 |
-
|
| 62 |
-
// Velocity with Exponential Moving Average (EMA) for smoothing
|
| 63 |
-
const cx0 = tr.bbox.x + tr.bbox.w * 0.5;
|
| 64 |
-
const cy0 = tr.bbox.y + tr.bbox.h * 0.5;
|
| 65 |
-
const cx1 = best.bbox.x + best.bbox.w * 0.5;
|
| 66 |
-
const cy1 = best.bbox.y + best.bbox.h * 0.5;
|
| 67 |
-
|
| 68 |
-
const rawVx = (cx1 - cx0) / Math.max(1e-3, dtSec);
|
| 69 |
-
const rawVy = (cy1 - cy0) / Math.max(1e-3, dtSec);
|
| 70 |
-
|
| 71 |
-
// Alpha of 0.3 means 30% new value, 70% history
|
| 72 |
-
tr.vx = tr.vx * 0.7 + rawVx * 0.3;
|
| 73 |
-
tr.vy = tr.vy * 0.7 + rawVy * 0.3;
|
| 74 |
-
|
| 75 |
-
// smooth bbox update
|
| 76 |
-
tr.bbox.x = lerp(tr.bbox.x, best.bbox.x, 0.7);
|
| 77 |
-
tr.bbox.y = lerp(tr.bbox.y, best.bbox.y, 0.7);
|
| 78 |
-
tr.bbox.w = lerp(tr.bbox.w, best.bbox.w, 0.6);
|
| 79 |
-
tr.bbox.h = lerp(tr.bbox.h, best.bbox.h, 0.6);
|
| 80 |
-
|
| 81 |
-
// Logic: Only update label if the new detection is highly confident
|
| 82 |
-
// AND the current track doesn't have a "premium" label (like 'drone').
|
| 83 |
-
const protectedLabels = ["drone", "uav", "missile"];
|
| 84 |
-
const isProtected = protectedLabels.some(l => (tr.label || "").toLowerCase().includes(l));
|
| 85 |
-
|
| 86 |
-
if (!isProtected || (best.label && protectedLabels.some(l => best.label.toLowerCase().includes(l)))) {
|
| 87 |
-
tr.label = best.label || tr.label;
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
tr.score = best.score || tr.score;
|
| 91 |
-
if (Number.isFinite(best.depth_rel)) {
|
| 92 |
-
tr.depth_rel = best.depth_rel;
|
| 93 |
-
}
|
| 94 |
-
if (best.depth_valid) {
|
| 95 |
-
// EMA Smoothing
|
| 96 |
-
const newD = best.depth_est_m;
|
| 97 |
-
if (tr.depth_est_m == null) tr.depth_est_m = newD;
|
| 98 |
-
else tr.depth_est_m = tr.depth_est_m * 0.7 + newD * 0.3;
|
| 99 |
-
tr.depth_valid = true;
|
| 100 |
-
}
|
| 101 |
-
tr.lastSeen = now();
|
| 102 |
-
} else {
|
| 103 |
-
// Decay velocity
|
| 104 |
-
tr.vx *= 0.9;
|
| 105 |
-
tr.vy *= 0.9;
|
| 106 |
-
}
|
| 107 |
-
}
|
| 108 |
-
|
| 109 |
-
// Limit total tracks
|
| 110 |
-
if (tracks.length < CONFIG.MAX_TRACKS) {
|
| 111 |
-
for (let i = 0; i < detObjs.length; i++) {
|
| 112 |
-
if (used.has(i)) continue;
|
| 113 |
-
// create new track only if big enough
|
| 114 |
-
const a = detObjs[i].bbox.w * detObjs[i].bbox.h;
|
| 115 |
-
if (a < (w * h) * 0.0005) continue;
|
| 116 |
-
|
| 117 |
-
const newId = `T${String(state.tracker.nextId++).padStart(2, "0")}`;
|
| 118 |
-
const ap = defaultAimpoint(detObjs[i].label);
|
| 119 |
-
tracks.push({
|
| 120 |
-
id: newId,
|
| 121 |
-
label: detObjs[i].label,
|
| 122 |
-
bbox: { ...detObjs[i].bbox },
|
| 123 |
-
score: detObjs[i].score,
|
| 124 |
-
aimRel: { relx: ap.relx, rely: ap.rely, label: ap.label },
|
| 125 |
-
baseAreaFrac: (detObjs[i].bbox.w * detObjs[i].bbox.h) / (w * h),
|
| 126 |
-
baseRange_m: rangeBase ? +rangeBase.value : 1000,
|
| 127 |
-
baseDwell_s: 5.5,
|
| 128 |
-
reqP_kW: 42,
|
| 129 |
-
depth_rel: detObjs[i].depth_rel,
|
| 130 |
-
depth_est_m: detObjs[i].depth_est_m,
|
| 131 |
-
depth_valid: detObjs[i].depth_valid,
|
| 132 |
-
|
| 133 |
-
// GPT properties
|
| 134 |
-
gpt_distance_m: null,
|
| 135 |
-
gpt_direction: null,
|
| 136 |
-
gpt_description: null,
|
| 137 |
-
|
| 138 |
-
// Track state
|
| 139 |
-
lastSeen: now(),
|
| 140 |
-
vx: 0, vy: 0,
|
| 141 |
-
dwellAccum: 0,
|
| 142 |
-
killed: false,
|
| 143 |
-
state: "TRACK",
|
| 144 |
-
assessT: 0
|
| 145 |
-
});
|
| 146 |
-
log(`New track created: ${newId} (${detObjs[i].label})`, "t");
|
| 147 |
-
}
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
// prune old tracks
|
| 151 |
-
const tNow = now();
|
| 152 |
-
state.tracker.tracks = tracks.filter(tr => (tNow - tr.lastSeen) < CONFIG.TRACK_PRUNE_MS || tr.killed);
|
| 153 |
-
};
|
| 154 |
-
|
| 155 |
-
// Polling for backend tracks
|
| 156 |
-
APP.core.tracker.syncWithBackend = async function (frameIdx) {
|
| 157 |
-
const { state } = APP.core;
|
| 158 |
-
const { $ } = APP.core.utils;
|
| 159 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 160 |
-
|
| 161 |
-
if (!jobId || !state.hf.baseUrl) return;
|
| 162 |
-
|
| 163 |
-
try {
|
| 164 |
-
const resp = await fetch(`${state.hf.baseUrl}/detect/tracks/${jobId}/${frameIdx}`);
|
| 165 |
-
if (!resp.ok) return;
|
| 166 |
-
|
| 167 |
-
const dets = await resp.json();
|
| 168 |
-
if (!dets || !Array.isArray(dets)) return;
|
| 169 |
-
|
| 170 |
-
// Transform backend format to frontend track format
|
| 171 |
-
// Backend: { bbox: [x1, y1, x2, y2], label: "car", track_id: "T01", angle_deg: 90, ... }
|
| 172 |
-
// Frontend: { id: "T01", bbox: {x,y,w,h}, label: "car", angle_deg: 90, ... }
|
| 173 |
-
|
| 174 |
-
const videoEngage = $("#videoEngage");
|
| 175 |
-
const w = videoEngage ? (videoEngage.videoWidth || state.frame.w) : state.frame.w;
|
| 176 |
-
const h = videoEngage ? (videoEngage.videoHeight || state.frame.h) : state.frame.h;
|
| 177 |
-
|
| 178 |
-
const newTracks = dets.map(d => {
|
| 179 |
-
const x = d.bbox[0], y = d.bbox[1];
|
| 180 |
-
const wBox = d.bbox[2] - d.bbox[0];
|
| 181 |
-
const hBox = d.bbox[3] - d.bbox[1];
|
| 182 |
-
|
| 183 |
-
// Normalize
|
| 184 |
-
const nx = x / w;
|
| 185 |
-
const ny = y / h;
|
| 186 |
-
const nw = wBox / w;
|
| 187 |
-
const nh = hBox / h;
|
| 188 |
-
|
| 189 |
-
return {
|
| 190 |
-
id: d.track_id || `T${Math.floor(Math.random() * 1000)}`, // Fallback
|
| 191 |
-
label: d.label,
|
| 192 |
-
bbox: { x: nx, y: ny, w: nw, h: nh },
|
| 193 |
-
score: d.score,
|
| 194 |
-
angle_deg: d.angle_deg,
|
| 195 |
-
gpt_distance_m: d.gpt_distance_m,
|
| 196 |
-
gpt_direction: d.gpt_direction,
|
| 197 |
-
gpt_description: d.gpt_description,
|
| 198 |
-
speed_kph: d.speed_kph,
|
| 199 |
-
depth_est_m: d.depth_est_m,
|
| 200 |
-
depth_rel: d.depth_rel,
|
| 201 |
-
depth_valid: d.depth_valid,
|
| 202 |
-
// Threat intelligence
|
| 203 |
-
threat_level_score: d.threat_level_score || 0,
|
| 204 |
-
threat_classification: d.threat_classification || "Unknown",
|
| 205 |
-
weapon_readiness: d.weapon_readiness || "Unknown",
|
| 206 |
-
// Mission relevance and assessment status
|
| 207 |
-
mission_relevant: d.mission_relevant ?? null,
|
| 208 |
-
relevance_reason: d.relevance_reason || null,
|
| 209 |
-
assessment_status: d.assessment_status || APP.core.gptMapping.STATUS.UNASSESSED,
|
| 210 |
-
assessment_frame_index: d.assessment_frame_index ?? null,
|
| 211 |
-
// GPT raw data for feature table
|
| 212 |
-
gpt_raw: d.gpt_raw || null,
|
| 213 |
-
features: APP.core.gptMapping.buildFeatures(d.gpt_raw),
|
| 214 |
-
// Keep UI state fields
|
| 215 |
-
lastSeen: Date.now(),
|
| 216 |
-
state: "TRACK"
|
| 217 |
-
};
|
| 218 |
-
});
|
| 219 |
-
|
| 220 |
-
// Preserve GPT enrichment data that syncWithBackend would otherwise wipe
|
| 221 |
-
const gptCache = state.hf.firstFrameDetections;
|
| 222 |
-
if (gptCache && gptCache.length > 0) {
|
| 223 |
-
const gptMap = {};
|
| 224 |
-
for (const gd of gptCache) {
|
| 225 |
-
const tid = gd.track_id || `T${String(gptCache.indexOf(gd) + 1).padStart(2, "0")}`;
|
| 226 |
-
if (gd.gpt_raw) gptMap[tid] = gd;
|
| 227 |
-
}
|
| 228 |
-
for (const track of newTracks) {
|
| 229 |
-
const cached = gptMap[track.id];
|
| 230 |
-
if (!cached || track.gpt_raw) continue;
|
| 231 |
-
const g = cached.gpt_raw;
|
| 232 |
-
track.gpt_raw = g;
|
| 233 |
-
track.assessment_status = cached.assessment_status || APP.core.gptMapping.STATUS.ASSESSED;
|
| 234 |
-
track.threat_level_score = cached.threat_level_score || g.threat_level_score || 0;
|
| 235 |
-
track.threat_classification = cached.threat_classification || g.threat_classification || "Unknown";
|
| 236 |
-
track.weapon_readiness = cached.weapon_readiness || g.weapon_readiness || "Unknown";
|
| 237 |
-
track.gpt_distance_m = cached.gpt_distance_m || null;
|
| 238 |
-
track.gpt_direction = cached.gpt_direction || null;
|
| 239 |
-
track.mission_relevant = cached.mission_relevant ?? track.mission_relevant;
|
| 240 |
-
track.relevance_reason = cached.relevance_reason || track.relevance_reason;
|
| 241 |
-
track.features = APP.core.gptMapping.buildFeatures(g);
|
| 242 |
-
}
|
| 243 |
-
}
|
| 244 |
-
|
| 245 |
-
// Detect new objects before state update
|
| 246 |
-
const oldIds = new Set(state.tracker.tracks.map(t => t.id));
|
| 247 |
-
const brandNew = newTracks.filter(t => !oldIds.has(t.id));
|
| 248 |
-
if (brandNew.length > 0) {
|
| 249 |
-
state.tracker._newObjectDetected = true;
|
| 250 |
-
APP.ui.logging.log(`New objects: ${brandNew.map(t => t.id).join(", ")}`, "t");
|
| 251 |
-
}
|
| 252 |
-
|
| 253 |
-
// Update state
|
| 254 |
-
state.tracker.tracks = newTracks;
|
| 255 |
-
state.detections = newTracks; // Keep synced
|
| 256 |
-
|
| 257 |
-
} catch (e) {
|
| 258 |
-
console.warn("Track sync failed", e);
|
| 259 |
-
}
|
| 260 |
-
};
|
| 261 |
-
|
| 262 |
-
APP.core.tracker.predictTracks = function (dtSec) {
|
| 263 |
-
const { state } = APP.core;
|
| 264 |
-
const { $ } = APP.core.utils;
|
| 265 |
-
const videoEngage = $("#videoEngage");
|
| 266 |
-
if (!videoEngage) return;
|
| 267 |
-
const w = videoEngage.videoWidth || state.frame.w;
|
| 268 |
-
const h = videoEngage.videoHeight || state.frame.h;
|
| 269 |
-
|
| 270 |
-
// Simple clamp util locally or imported
|
| 271 |
-
const clamp = (val, min, max) => Math.min(max, Math.max(min, val));
|
| 272 |
-
|
| 273 |
-
state.tracker.tracks.forEach(tr => {
|
| 274 |
-
if (tr.killed) return;
|
| 275 |
-
tr.bbox.x = clamp(tr.bbox.x + tr.vx * dtSec * 0.12, 0, w - 1);
|
| 276 |
-
tr.bbox.y = clamp(tr.bbox.y + tr.vy * dtSec * 0.12, 0, h - 1);
|
| 277 |
-
});
|
| 278 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/utils.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
APP.core.utils = {};
|
| 2 |
-
|
| 3 |
-
APP.core.utils.$ = (sel, root = document) => root.querySelector(sel);
|
| 4 |
-
APP.core.utils.$$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
|
| 5 |
-
|
| 6 |
-
APP.core.utils.clamp = (x, a, b) => Math.min(b, Math.max(a, x));
|
| 7 |
-
APP.core.utils.lerp = (a, b, t) => a + (b - a) * t;
|
| 8 |
-
APP.core.utils.now = () => performance.now();
|
| 9 |
-
|
| 10 |
-
APP.core.utils.escapeHtml = function (s) {
|
| 11 |
-
return String(s).replace(/[&<>"']/g, m => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[m]));
|
| 12 |
-
};
|
| 13 |
-
|
| 14 |
-
APP.core.utils.canvasToBlob = function (canvas, quality = 0.88) {
|
| 15 |
-
return new Promise((resolve, reject) => {
|
| 16 |
-
if (!canvas.toBlob) { reject(new Error("Canvas.toBlob not supported")); return; }
|
| 17 |
-
canvas.toBlob(blob => {
|
| 18 |
-
if (!blob) { reject(new Error("Canvas toBlob failed")); return; }
|
| 19 |
-
resolve(blob);
|
| 20 |
-
}, "image/jpeg", quality);
|
| 21 |
-
});
|
| 22 |
-
};
|
| 23 |
-
|
| 24 |
-
APP.core.utils.normBBox = function (bbox, w, h) {
|
| 25 |
-
const [x, y, bw, bh] = bbox;
|
| 26 |
-
return {
|
| 27 |
-
x: APP.core.utils.clamp(x, 0, w - 1),
|
| 28 |
-
y: APP.core.utils.clamp(y, 0, h - 1),
|
| 29 |
-
w: APP.core.utils.clamp(bw, 1, w),
|
| 30 |
-
h: APP.core.utils.clamp(bh, 1, h)
|
| 31 |
-
};
|
| 32 |
-
};
|
| 33 |
-
|
| 34 |
-
APP.core.utils.loadedScripts = new Map();
|
| 35 |
-
|
| 36 |
-
APP.core.utils.loadScriptOnce = function (key, src) {
|
| 37 |
-
return new Promise((resolve, reject) => {
|
| 38 |
-
if (APP.core.utils.loadedScripts.get(key) === "loaded") { resolve(); return; }
|
| 39 |
-
if (APP.core.utils.loadedScripts.get(key) === "loading") {
|
| 40 |
-
const iv = setInterval(() => {
|
| 41 |
-
if (APP.core.utils.loadedScripts.get(key) === "loaded") { clearInterval(iv); resolve(); }
|
| 42 |
-
if (APP.core.utils.loadedScripts.get(key) === "failed") { clearInterval(iv); reject(new Error("Script failed earlier")); }
|
| 43 |
-
}, 50);
|
| 44 |
-
return;
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
APP.core.utils.loadedScripts.set(key, "loading");
|
| 48 |
-
const s = document.createElement("script");
|
| 49 |
-
s.src = src;
|
| 50 |
-
s.async = true;
|
| 51 |
-
s.onload = () => { APP.core.utils.loadedScripts.set(key, "loaded"); resolve(); };
|
| 52 |
-
s.onerror = () => { APP.core.utils.loadedScripts.set(key, "failed"); reject(new Error(`Failed to load ${src}`)); };
|
| 53 |
-
document.head.appendChild(s);
|
| 54 |
-
});
|
| 55 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/core/video.js
DELETED
|
@@ -1,621 +0,0 @@
|
|
| 1 |
-
// Video management: loading, unloading, first frame capture, depth handling
|
| 2 |
-
APP.core.video = {};
|
| 3 |
-
|
| 4 |
-
APP.core.video.captureFirstFrame = async function () {
|
| 5 |
-
const { state } = APP.core;
|
| 6 |
-
const { $ } = APP.core.utils;
|
| 7 |
-
const videoHidden = $("#videoHidden");
|
| 8 |
-
|
| 9 |
-
if (!videoHidden || !state.videoUrl) return;
|
| 10 |
-
|
| 11 |
-
videoHidden.src = state.videoUrl;
|
| 12 |
-
videoHidden.load();
|
| 13 |
-
|
| 14 |
-
await new Promise((resolve, reject) => {
|
| 15 |
-
videoHidden.onloadeddata = resolve;
|
| 16 |
-
videoHidden.onerror = () => reject(new Error("Video failed to load"));
|
| 17 |
-
setTimeout(() => reject(new Error("Video load timeout")), 10000);
|
| 18 |
-
});
|
| 19 |
-
|
| 20 |
-
// Seek to first frame
|
| 21 |
-
videoHidden.currentTime = 0;
|
| 22 |
-
await new Promise(r => {
|
| 23 |
-
videoHidden.onseeked = r;
|
| 24 |
-
setTimeout(r, 500);
|
| 25 |
-
});
|
| 26 |
-
|
| 27 |
-
state.frame.w = videoHidden.videoWidth || 1280;
|
| 28 |
-
state.frame.h = videoHidden.videoHeight || 720;
|
| 29 |
-
};
|
| 30 |
-
|
| 31 |
-
APP.core.video.drawFirstFrame = function () {
|
| 32 |
-
const { state } = APP.core;
|
| 33 |
-
const { $ } = APP.core.utils;
|
| 34 |
-
const frameCanvas = $("#frameCanvas");
|
| 35 |
-
const frameOverlay = $("#frameOverlay");
|
| 36 |
-
const videoHidden = $("#videoHidden");
|
| 37 |
-
const frameEmpty = $("#frameEmpty");
|
| 38 |
-
|
| 39 |
-
if (!frameCanvas || !videoHidden) return;
|
| 40 |
-
|
| 41 |
-
const ctx = frameCanvas.getContext("2d");
|
| 42 |
-
frameCanvas.width = state.frame.w;
|
| 43 |
-
frameCanvas.height = state.frame.h;
|
| 44 |
-
ctx.drawImage(videoHidden, 0, 0, state.frame.w, state.frame.h);
|
| 45 |
-
|
| 46 |
-
// Also resize overlay to match so bbox coordinates align
|
| 47 |
-
if (frameOverlay) {
|
| 48 |
-
frameOverlay.width = state.frame.w;
|
| 49 |
-
frameOverlay.height = state.frame.h;
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
if (frameEmpty) frameEmpty.style.display = "none";
|
| 53 |
-
};
|
| 54 |
-
|
| 55 |
-
APP.core.video.frameToBitmap = async function (videoEl) {
|
| 56 |
-
const w = videoEl.videoWidth || 1280;
|
| 57 |
-
const h = videoEl.videoHeight || 720;
|
| 58 |
-
const canvas = document.createElement("canvas");
|
| 59 |
-
canvas.width = w;
|
| 60 |
-
canvas.height = h;
|
| 61 |
-
const ctx = canvas.getContext("2d");
|
| 62 |
-
ctx.drawImage(videoEl, 0, 0, w, h);
|
| 63 |
-
return canvas;
|
| 64 |
-
};
|
| 65 |
-
|
| 66 |
-
APP.core.video.seekTo = function (videoEl, time) {
|
| 67 |
-
return new Promise((resolve) => {
|
| 68 |
-
if (!videoEl) { resolve(); return; }
|
| 69 |
-
videoEl.currentTime = Math.max(0, time);
|
| 70 |
-
videoEl.onseeked = () => resolve();
|
| 71 |
-
setTimeout(resolve, 600);
|
| 72 |
-
});
|
| 73 |
-
};
|
| 74 |
-
|
| 75 |
-
APP.core.video.unloadVideo = async function (options = {}) {
|
| 76 |
-
const { state } = APP.core;
|
| 77 |
-
const { $, $$ } = APP.core.utils;
|
| 78 |
-
const { log, setStatus, setHfStatus } = APP.ui.logging;
|
| 79 |
-
const preserveInput = !!options.preserveInput;
|
| 80 |
-
|
| 81 |
-
// Stop polling if running
|
| 82 |
-
if (state.hf.asyncPollInterval) {
|
| 83 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 84 |
-
state.hf.asyncPollInterval = null;
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
// Revoke blob URLs
|
| 88 |
-
if (state.videoUrl && state.videoUrl.startsWith("blob:")) {
|
| 89 |
-
URL.revokeObjectURL(state.videoUrl);
|
| 90 |
-
}
|
| 91 |
-
if (state.hf.processedUrl && state.hf.processedUrl.startsWith("blob:")) {
|
| 92 |
-
try { URL.revokeObjectURL(state.hf.processedUrl); } catch (_) { }
|
| 93 |
-
}
|
| 94 |
-
if (state.hf.depthVideoUrl && state.hf.depthVideoUrl.startsWith("blob:")) {
|
| 95 |
-
try { URL.revokeObjectURL(state.hf.depthVideoUrl); } catch (_) { }
|
| 96 |
-
}
|
| 97 |
-
if (state.hf.depthFirstFrameUrl && state.hf.depthFirstFrameUrl.startsWith("blob:")) {
|
| 98 |
-
try { URL.revokeObjectURL(state.hf.depthFirstFrameUrl); } catch (_) { }
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
// Reset state
|
| 102 |
-
state.videoUrl = null;
|
| 103 |
-
state.videoFile = null;
|
| 104 |
-
state.videoLoaded = false;
|
| 105 |
-
state.useProcessedFeed = false;
|
| 106 |
-
state.useDepthFeed = false;
|
| 107 |
-
state.useFrameDepthView = false;
|
| 108 |
-
|
| 109 |
-
state.hf.missionId = null;
|
| 110 |
-
state.hf.plan = null;
|
| 111 |
-
state.hf.processedUrl = null;
|
| 112 |
-
state.hf.processedBlob = null;
|
| 113 |
-
state.hf.depthVideoUrl = null;
|
| 114 |
-
state.hf.depthBlob = null;
|
| 115 |
-
state.hf.depthFirstFrameUrl = null;
|
| 116 |
-
state.hf.depthFirstFrameBlob = null;
|
| 117 |
-
state.hf.summary = null;
|
| 118 |
-
state.hf.busy = false;
|
| 119 |
-
state.hf.lastError = null;
|
| 120 |
-
state.hf.asyncJobId = null;
|
| 121 |
-
state.hf.completedJobId = null;
|
| 122 |
-
state.hf.asyncStatus = "idle";
|
| 123 |
-
state.hf.firstFrameUrl = null;
|
| 124 |
-
state.hf.videoUrl = null;
|
| 125 |
-
|
| 126 |
-
setHfStatus("idle");
|
| 127 |
-
state.hasReasoned = false;
|
| 128 |
-
state.firstFrameReady = false;
|
| 129 |
-
state.isReasoning = false;
|
| 130 |
-
|
| 131 |
-
// Reset button states
|
| 132 |
-
const btnReason = $("#btnReason");
|
| 133 |
-
const btnCancelReason = $("#btnCancelReason");
|
| 134 |
-
const btnEngage = $("#btnEngage");
|
| 135 |
-
|
| 136 |
-
if (btnReason) {
|
| 137 |
-
btnReason.disabled = false;
|
| 138 |
-
btnReason.style.opacity = "1";
|
| 139 |
-
btnReason.style.cursor = "pointer";
|
| 140 |
-
}
|
| 141 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 142 |
-
if (btnEngage) btnEngage.disabled = true;
|
| 143 |
-
|
| 144 |
-
state.detections = [];
|
| 145 |
-
state.selectedId = null;
|
| 146 |
-
|
| 147 |
-
state.tracker.tracks = [];
|
| 148 |
-
state.tracker.nextId = 1;
|
| 149 |
-
state.tracker.running = false;
|
| 150 |
-
state.tracker.selectedTrackId = null;
|
| 151 |
-
state.tracker.beamOn = false;
|
| 152 |
-
|
| 153 |
-
// Clear video elements
|
| 154 |
-
const videoHidden = $("#videoHidden");
|
| 155 |
-
const videoEngage = $("#videoEngage");
|
| 156 |
-
const videoFile = $("#videoFile");
|
| 157 |
-
|
| 158 |
-
if (videoHidden) {
|
| 159 |
-
videoHidden.removeAttribute("src");
|
| 160 |
-
videoHidden.load();
|
| 161 |
-
}
|
| 162 |
-
if (videoEngage) {
|
| 163 |
-
videoEngage.removeAttribute("src");
|
| 164 |
-
videoEngage.load();
|
| 165 |
-
}
|
| 166 |
-
if (!preserveInput && videoFile) {
|
| 167 |
-
videoFile.value = "";
|
| 168 |
-
}
|
| 169 |
-
|
| 170 |
-
// Update UI
|
| 171 |
-
const videoMeta = $("#videoMeta");
|
| 172 |
-
const frameEmpty = $("#frameEmpty");
|
| 173 |
-
const engageEmpty = $("#engageEmpty");
|
| 174 |
-
const frameNote = $("#frameNote");
|
| 175 |
-
const engageNote = $("#engageNote");
|
| 176 |
-
|
| 177 |
-
if (!preserveInput && videoMeta) videoMeta.textContent = "No file";
|
| 178 |
-
if (frameEmpty) frameEmpty.style.display = "flex";
|
| 179 |
-
if (engageEmpty) engageEmpty.style.display = "flex";
|
| 180 |
-
if (frameNote) frameNote.textContent = "Awaiting video";
|
| 181 |
-
if (engageNote) engageNote.textContent = "Awaiting video";
|
| 182 |
-
|
| 183 |
-
// Clear canvases
|
| 184 |
-
APP.core.video.clearCanvas($("#frameCanvas"));
|
| 185 |
-
APP.core.video.clearCanvas($("#frameOverlay"));
|
| 186 |
-
APP.core.video.clearCanvas($("#engageOverlay"));
|
| 187 |
-
|
| 188 |
-
// Re-render UI components
|
| 189 |
-
if (APP.ui.cards.renderFrameTrackList) APP.ui.cards.renderFrameTrackList();
|
| 190 |
-
|
| 191 |
-
setStatus("warn", "STANDBY · No video loaded");
|
| 192 |
-
log("Video unloaded. Demo reset.", "w");
|
| 193 |
-
};
|
| 194 |
-
|
| 195 |
-
APP.core.video.clearCanvas = function (canvas) {
|
| 196 |
-
if (!canvas) return;
|
| 197 |
-
const ctx = canvas.getContext("2d");
|
| 198 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 199 |
-
};
|
| 200 |
-
|
| 201 |
-
APP.core.video.resizeOverlays = function () {
|
| 202 |
-
const { state } = APP.core;
|
| 203 |
-
const { $ } = APP.core.utils;
|
| 204 |
-
|
| 205 |
-
const videoEngage = $("#videoEngage");
|
| 206 |
-
const engageOverlay = $("#engageOverlay");
|
| 207 |
-
const frameOverlay = $("#frameOverlay");
|
| 208 |
-
|
| 209 |
-
if (videoEngage && engageOverlay) {
|
| 210 |
-
const w = videoEngage.videoWidth || state.frame.w;
|
| 211 |
-
const h = videoEngage.videoHeight || state.frame.h;
|
| 212 |
-
engageOverlay.width = w;
|
| 213 |
-
engageOverlay.height = h;
|
| 214 |
-
engageOverlay.style.pointerEvents = "auto";
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
if (frameOverlay) {
|
| 218 |
-
frameOverlay.width = state.frame.w;
|
| 219 |
-
frameOverlay.height = state.frame.h;
|
| 220 |
-
}
|
| 221 |
-
};
|
| 222 |
-
|
| 223 |
-
// Depth video handling
|
| 224 |
-
APP.core.video.fetchDepthVideo = async function () {
|
| 225 |
-
const { state } = APP.core;
|
| 226 |
-
const { log } = APP.ui.logging;
|
| 227 |
-
|
| 228 |
-
// Depth is optional - skip silently if no URL
|
| 229 |
-
if (!state.hf.depthVideoUrl) {
|
| 230 |
-
return;
|
| 231 |
-
}
|
| 232 |
-
|
| 233 |
-
try {
|
| 234 |
-
const resp = await fetch(state.hf.depthVideoUrl, { cache: "no-store" });
|
| 235 |
-
|
| 236 |
-
if (!resp.ok) {
|
| 237 |
-
// 404 = depth not enabled/available - this is fine, not an error
|
| 238 |
-
if (resp.status === 404) {
|
| 239 |
-
state.hf.depthVideoUrl = null;
|
| 240 |
-
return;
|
| 241 |
-
}
|
| 242 |
-
// 202 = still processing
|
| 243 |
-
if (resp.status === 202) {
|
| 244 |
-
return;
|
| 245 |
-
}
|
| 246 |
-
throw new Error(`Failed to fetch depth video: ${resp.statusText}`);
|
| 247 |
-
}
|
| 248 |
-
|
| 249 |
-
const nullOrigin = (window.location && window.location.origin) === "null" || (window.location && window.location.protocol === "file:");
|
| 250 |
-
if (nullOrigin) {
|
| 251 |
-
state.hf.depthBlob = null;
|
| 252 |
-
state.hf.depthVideoUrl = `${state.hf.depthVideoUrl}?t=${Date.now()}`;
|
| 253 |
-
log("Depth video ready (streaming URL)");
|
| 254 |
-
return;
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
const blob = await resp.blob();
|
| 258 |
-
state.hf.depthBlob = blob;
|
| 259 |
-
const blobUrl = URL.createObjectURL(blob);
|
| 260 |
-
state.hf.depthVideoUrl = blobUrl;
|
| 261 |
-
|
| 262 |
-
log(`Depth video ready (${(blob.size / 1024 / 1024).toFixed(1)} MB)`, "g");
|
| 263 |
-
APP.core.video.updateDepthChip();
|
| 264 |
-
} catch (err) {
|
| 265 |
-
// Don't log as error - depth is optional
|
| 266 |
-
state.hf.depthVideoUrl = null;
|
| 267 |
-
state.hf.depthBlob = null;
|
| 268 |
-
}
|
| 269 |
-
};
|
| 270 |
-
|
| 271 |
-
APP.core.video.fetchDepthFirstFrame = async function () {
|
| 272 |
-
const { state } = APP.core;
|
| 273 |
-
const { log } = APP.ui.logging;
|
| 274 |
-
|
| 275 |
-
// Depth is optional - skip silently if no URL
|
| 276 |
-
if (!state.hf.depthFirstFrameUrl) return;
|
| 277 |
-
|
| 278 |
-
try {
|
| 279 |
-
const resp = await fetch(state.hf.depthFirstFrameUrl, { cache: "no-store" });
|
| 280 |
-
|
| 281 |
-
// 404 or other errors - depth not available, that's fine
|
| 282 |
-
if (!resp.ok) {
|
| 283 |
-
state.hf.depthFirstFrameUrl = null;
|
| 284 |
-
return;
|
| 285 |
-
}
|
| 286 |
-
|
| 287 |
-
const blob = await resp.blob();
|
| 288 |
-
state.hf.depthFirstFrameBlob = blob;
|
| 289 |
-
state.hf.depthFirstFrameUrl = URL.createObjectURL(blob);
|
| 290 |
-
log("First frame depth ready", "g");
|
| 291 |
-
} catch (err) {
|
| 292 |
-
// Silently clear - depth is optional
|
| 293 |
-
state.hf.depthFirstFrameUrl = null;
|
| 294 |
-
state.hf.depthFirstFrameBlob = null;
|
| 295 |
-
}
|
| 296 |
-
};
|
| 297 |
-
|
| 298 |
-
APP.core.video.fetchProcessedVideo = async function () {
|
| 299 |
-
const { state } = APP.core;
|
| 300 |
-
const { log } = APP.ui.logging;
|
| 301 |
-
const { $ } = APP.core.utils;
|
| 302 |
-
|
| 303 |
-
const resp = await fetch(state.hf.videoUrl, { cache: "no-store" });
|
| 304 |
-
|
| 305 |
-
if (!resp.ok) {
|
| 306 |
-
if (resp.status === 202) {
|
| 307 |
-
const err = new Error("Video still processing");
|
| 308 |
-
err.code = "VIDEO_PENDING";
|
| 309 |
-
throw err;
|
| 310 |
-
}
|
| 311 |
-
throw new Error(`Failed to fetch video: ${resp.statusText}`);
|
| 312 |
-
}
|
| 313 |
-
|
| 314 |
-
const nullOrigin = (window.location && window.location.origin) === "null" || (window.location && window.location.protocol === "file:");
|
| 315 |
-
if (nullOrigin) {
|
| 316 |
-
state.hf.processedBlob = null;
|
| 317 |
-
state.hf.processedUrl = `${state.hf.videoUrl}?t=${Date.now()}`;
|
| 318 |
-
const btnEngage = $("#btnEngage");
|
| 319 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 320 |
-
log("Processed video ready (streaming URL)");
|
| 321 |
-
return;
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
const blob = await resp.blob();
|
| 325 |
-
|
| 326 |
-
if (state.hf.processedUrl && state.hf.processedUrl.startsWith("blob:")) {
|
| 327 |
-
URL.revokeObjectURL(state.hf.processedUrl);
|
| 328 |
-
}
|
| 329 |
-
|
| 330 |
-
state.hf.processedBlob = blob;
|
| 331 |
-
state.hf.processedUrl = URL.createObjectURL(blob);
|
| 332 |
-
|
| 333 |
-
const btnEngage = $("#btnEngage");
|
| 334 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 335 |
-
log(`Processed video ready (${(blob.size / 1024 / 1024).toFixed(1)} MB)`);
|
| 336 |
-
};
|
| 337 |
-
|
| 338 |
-
APP.core.video.updateDepthChip = function () {
|
| 339 |
-
const { state } = APP.core;
|
| 340 |
-
const { $ } = APP.core.utils;
|
| 341 |
-
|
| 342 |
-
const chipDepth = $("#chipDepth");
|
| 343 |
-
if (!chipDepth) return;
|
| 344 |
-
|
| 345 |
-
if (state.hf.depthVideoUrl || state.hf.depthBlob) {
|
| 346 |
-
chipDepth.style.cursor = "pointer";
|
| 347 |
-
chipDepth.style.opacity = "1";
|
| 348 |
-
} else {
|
| 349 |
-
chipDepth.style.cursor = "not-allowed";
|
| 350 |
-
chipDepth.style.opacity = "0.5";
|
| 351 |
-
}
|
| 352 |
-
};
|
| 353 |
-
|
| 354 |
-
APP.core.video.toggleDepthView = function () {
|
| 355 |
-
const { state } = APP.core;
|
| 356 |
-
const { $, log } = APP.core.utils;
|
| 357 |
-
const { log: uiLog } = APP.ui.logging;
|
| 358 |
-
|
| 359 |
-
if (!state.hf.depthVideoUrl && !state.hf.depthBlob) {
|
| 360 |
-
uiLog("Depth video not available yet. Run Reason and wait for processing.", "w");
|
| 361 |
-
return;
|
| 362 |
-
}
|
| 363 |
-
|
| 364 |
-
state.useDepthFeed = !state.useDepthFeed;
|
| 365 |
-
|
| 366 |
-
const videoEngage = $("#videoEngage");
|
| 367 |
-
const chipDepth = $("#chipDepth");
|
| 368 |
-
|
| 369 |
-
if (state.useDepthFeed) {
|
| 370 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEPTH";
|
| 371 |
-
if (videoEngage) {
|
| 372 |
-
const currentTime = videoEngage.currentTime;
|
| 373 |
-
const wasPlaying = !videoEngage.paused;
|
| 374 |
-
videoEngage.src = state.hf.depthVideoUrl;
|
| 375 |
-
videoEngage.load();
|
| 376 |
-
videoEngage.currentTime = currentTime;
|
| 377 |
-
if (wasPlaying) videoEngage.play();
|
| 378 |
-
}
|
| 379 |
-
} else {
|
| 380 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEFAULT";
|
| 381 |
-
if (videoEngage) {
|
| 382 |
-
const currentTime = videoEngage.currentTime;
|
| 383 |
-
const wasPlaying = !videoEngage.paused;
|
| 384 |
-
const feedUrl = state.useProcessedFeed ? state.hf.processedUrl : state.videoUrl;
|
| 385 |
-
videoEngage.src = feedUrl;
|
| 386 |
-
videoEngage.load();
|
| 387 |
-
videoEngage.currentTime = currentTime;
|
| 388 |
-
if (wasPlaying) videoEngage.play();
|
| 389 |
-
}
|
| 390 |
-
}
|
| 391 |
-
};
|
| 392 |
-
|
| 393 |
-
APP.core.video.toggleFirstFrameDepthView = function () {
|
| 394 |
-
const { state } = APP.core;
|
| 395 |
-
const { $ } = APP.core.utils;
|
| 396 |
-
const { log } = APP.ui.logging;
|
| 397 |
-
|
| 398 |
-
if (!state.hf.depthFirstFrameUrl) {
|
| 399 |
-
log("First frame depth not available", "w");
|
| 400 |
-
return;
|
| 401 |
-
}
|
| 402 |
-
|
| 403 |
-
state.useFrameDepthView = !state.useFrameDepthView;
|
| 404 |
-
|
| 405 |
-
const frameCanvas = $("#frameCanvas");
|
| 406 |
-
const chipFrameDepth = $("#chipFrameDepth");
|
| 407 |
-
|
| 408 |
-
if (state.useFrameDepthView) {
|
| 409 |
-
if (chipFrameDepth) chipFrameDepth.textContent = "VIEW:DEPTH";
|
| 410 |
-
// Draw depth first frame
|
| 411 |
-
const img = new Image();
|
| 412 |
-
img.onload = () => {
|
| 413 |
-
if (frameCanvas) {
|
| 414 |
-
frameCanvas.width = state.frame.w;
|
| 415 |
-
frameCanvas.height = state.frame.h;
|
| 416 |
-
frameCanvas.getContext("2d").drawImage(img, 0, 0, state.frame.w, state.frame.h);
|
| 417 |
-
APP.ui.overlays.renderFrameOverlay();
|
| 418 |
-
}
|
| 419 |
-
};
|
| 420 |
-
img.src = state.hf.depthFirstFrameUrl;
|
| 421 |
-
} else {
|
| 422 |
-
if (chipFrameDepth) chipFrameDepth.textContent = "VIEW:DEFAULT";
|
| 423 |
-
// Re-draw original first frame
|
| 424 |
-
APP.core.video.drawFirstFrame();
|
| 425 |
-
APP.ui.overlays.renderFrameOverlay();
|
| 426 |
-
}
|
| 427 |
-
};
|
| 428 |
-
|
| 429 |
-
APP.core.video.toggleProcessedFeed = function () {
|
| 430 |
-
const { state } = APP.core;
|
| 431 |
-
const { $ } = APP.core.utils;
|
| 432 |
-
const { log } = APP.ui.logging;
|
| 433 |
-
|
| 434 |
-
if (!state.hf.processedUrl) {
|
| 435 |
-
log("Processed video not available yet", "w");
|
| 436 |
-
return;
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
state.useProcessedFeed = !state.useProcessedFeed;
|
| 440 |
-
state.useDepthFeed = false; // Reset depth view when switching feeds
|
| 441 |
-
|
| 442 |
-
const videoEngage = $("#videoEngage");
|
| 443 |
-
const chipFeed = $("#chipFeed");
|
| 444 |
-
const chipDepth = $("#chipDepth");
|
| 445 |
-
|
| 446 |
-
if (state.useProcessedFeed) {
|
| 447 |
-
if (chipFeed) chipFeed.textContent = "FEED:HF";
|
| 448 |
-
if (videoEngage) {
|
| 449 |
-
const currentTime = videoEngage.currentTime;
|
| 450 |
-
const wasPlaying = !videoEngage.paused;
|
| 451 |
-
videoEngage.src = state.hf.processedUrl;
|
| 452 |
-
videoEngage.load();
|
| 453 |
-
videoEngage.currentTime = currentTime;
|
| 454 |
-
if (wasPlaying) videoEngage.play();
|
| 455 |
-
}
|
| 456 |
-
} else {
|
| 457 |
-
if (chipFeed) chipFeed.textContent = "FEED:RAW";
|
| 458 |
-
if (videoEngage) {
|
| 459 |
-
const currentTime = videoEngage.currentTime;
|
| 460 |
-
const wasPlaying = !videoEngage.paused;
|
| 461 |
-
videoEngage.src = state.videoUrl;
|
| 462 |
-
videoEngage.load();
|
| 463 |
-
videoEngage.currentTime = currentTime;
|
| 464 |
-
if (wasPlaying) videoEngage.play();
|
| 465 |
-
}
|
| 466 |
-
}
|
| 467 |
-
|
| 468 |
-
if (chipDepth) chipDepth.textContent = "VIEW:DEFAULT";
|
| 469 |
-
};
|
| 470 |
-
|
| 471 |
-
// ========= Streaming Mode for Tab 2 (Live Backend Processing) =========
|
| 472 |
-
|
| 473 |
-
APP.core.video.setStreamingMode = function (url) {
|
| 474 |
-
const { $ } = APP.core.utils;
|
| 475 |
-
const videoEngage = $("#videoEngage");
|
| 476 |
-
const engageEmpty = $("#engageEmpty");
|
| 477 |
-
|
| 478 |
-
// Ensure stream image element exists
|
| 479 |
-
let streamView = $("#streamView");
|
| 480 |
-
if (!streamView) {
|
| 481 |
-
streamView = document.createElement("img");
|
| 482 |
-
streamView.id = "streamView";
|
| 483 |
-
streamView.style.width = "100%";
|
| 484 |
-
streamView.style.height = "100%";
|
| 485 |
-
streamView.style.objectFit = "contain";
|
| 486 |
-
streamView.style.position = "absolute";
|
| 487 |
-
streamView.style.top = "0";
|
| 488 |
-
streamView.style.left = "0";
|
| 489 |
-
streamView.style.zIndex = "10"; // Above video
|
| 490 |
-
streamView.style.backgroundColor = "#000";
|
| 491 |
-
|
| 492 |
-
// Insert into the wrapper (parent of videoEngage)
|
| 493 |
-
if (videoEngage && videoEngage.parentNode) {
|
| 494 |
-
videoEngage.parentNode.appendChild(streamView);
|
| 495 |
-
// Ensure container is relative for absolute positioning
|
| 496 |
-
if (getComputedStyle(videoEngage.parentNode).position === "static") {
|
| 497 |
-
videoEngage.parentNode.style.position = "relative";
|
| 498 |
-
}
|
| 499 |
-
}
|
| 500 |
-
}
|
| 501 |
-
|
| 502 |
-
if (streamView) {
|
| 503 |
-
// Reset state
|
| 504 |
-
streamView.style.display = "block";
|
| 505 |
-
streamView.onerror = () => {
|
| 506 |
-
// If stream fails (404 etc), silently revert
|
| 507 |
-
streamView.style.display = "none";
|
| 508 |
-
if (videoEngage) videoEngage.style.display = "block";
|
| 509 |
-
if (engageEmpty && !videoEngage.src) engageEmpty.style.display = "flex";
|
| 510 |
-
};
|
| 511 |
-
streamView.src = url;
|
| 512 |
-
|
| 513 |
-
if (videoEngage) videoEngage.style.display = "none";
|
| 514 |
-
|
| 515 |
-
// Also hide empty state
|
| 516 |
-
if (engageEmpty) engageEmpty.style.display = "none";
|
| 517 |
-
}
|
| 518 |
-
};
|
| 519 |
-
|
| 520 |
-
APP.core.video.stopStreamingMode = function () {
|
| 521 |
-
const { $ } = APP.core.utils;
|
| 522 |
-
const videoEngage = $("#videoEngage");
|
| 523 |
-
|
| 524 |
-
const streamView = $("#streamView");
|
| 525 |
-
if (streamView) {
|
| 526 |
-
streamView.src = ""; // Stop connection
|
| 527 |
-
streamView.style.display = "none";
|
| 528 |
-
}
|
| 529 |
-
if (videoEngage) videoEngage.style.display = "block";
|
| 530 |
-
};
|
| 531 |
-
|
| 532 |
-
// ========= Display Processed First Frame (from backend) =========
|
| 533 |
-
|
| 534 |
-
APP.core.video.displayProcessedFirstFrame = function () {
|
| 535 |
-
const { state } = APP.core;
|
| 536 |
-
const { $ } = APP.core.utils;
|
| 537 |
-
const { log } = APP.ui.logging;
|
| 538 |
-
|
| 539 |
-
const frameCanvas = $("#frameCanvas");
|
| 540 |
-
const frameOverlay = $("#frameOverlay");
|
| 541 |
-
const frameEmpty = $("#frameEmpty");
|
| 542 |
-
const frameNote = $("#frameNote");
|
| 543 |
-
|
| 544 |
-
if (!state.hf.firstFrameUrl) {
|
| 545 |
-
log("Processed first frame URL not available", "w");
|
| 546 |
-
return;
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
const img = new Image();
|
| 550 |
-
img.crossOrigin = "anonymous";
|
| 551 |
-
img.onload = () => {
|
| 552 |
-
if (frameCanvas) {
|
| 553 |
-
// Update frame dimensions from image
|
| 554 |
-
state.frame.w = img.naturalWidth || 1280;
|
| 555 |
-
state.frame.h = img.naturalHeight || 720;
|
| 556 |
-
|
| 557 |
-
// Resize both canvas and overlay to match frame dimensions
|
| 558 |
-
frameCanvas.width = state.frame.w;
|
| 559 |
-
frameCanvas.height = state.frame.h;
|
| 560 |
-
frameCanvas.getContext("2d").drawImage(img, 0, 0, state.frame.w, state.frame.h);
|
| 561 |
-
|
| 562 |
-
// CRITICAL: Resize frameOverlay to match so bbox coordinates align
|
| 563 |
-
if (frameOverlay) {
|
| 564 |
-
frameOverlay.width = state.frame.w;
|
| 565 |
-
frameOverlay.height = state.frame.h;
|
| 566 |
-
}
|
| 567 |
-
|
| 568 |
-
// Hide empty state
|
| 569 |
-
if (frameEmpty) frameEmpty.style.display = "none";
|
| 570 |
-
if (frameNote) frameNote.textContent = "Processed (from backend)";
|
| 571 |
-
|
| 572 |
-
// Re-render overlay on top (now with matching dimensions)
|
| 573 |
-
if (APP.ui.overlays.renderFrameOverlay) {
|
| 574 |
-
APP.ui.overlays.renderFrameOverlay();
|
| 575 |
-
}
|
| 576 |
-
|
| 577 |
-
log("Processed first frame displayed", "g");
|
| 578 |
-
}
|
| 579 |
-
};
|
| 580 |
-
img.onerror = () => {
|
| 581 |
-
log("Failed to load processed first frame", "e");
|
| 582 |
-
// Fallback to local first frame
|
| 583 |
-
APP.core.video.drawFirstFrame();
|
| 584 |
-
};
|
| 585 |
-
img.src = state.hf.firstFrameUrl;
|
| 586 |
-
};
|
| 587 |
-
|
| 588 |
-
// ========= Display First Frame with Depth Overlay (if available) =========
|
| 589 |
-
|
| 590 |
-
APP.core.video.displayFirstFrameWithDepth = function () {
|
| 591 |
-
const { state } = APP.core;
|
| 592 |
-
const { $ } = APP.core.utils;
|
| 593 |
-
|
| 594 |
-
// Check if we're in depth view mode and depth is available
|
| 595 |
-
if (state.useFrameDepthView && state.hf.depthFirstFrameUrl) {
|
| 596 |
-
const frameCanvas = $("#frameCanvas");
|
| 597 |
-
const img = new Image();
|
| 598 |
-
img.crossOrigin = "anonymous";
|
| 599 |
-
img.onload = () => {
|
| 600 |
-
if (frameCanvas) {
|
| 601 |
-
frameCanvas.width = state.frame.w;
|
| 602 |
-
frameCanvas.height = state.frame.h;
|
| 603 |
-
frameCanvas.getContext("2d").drawImage(img, 0, 0, state.frame.w, state.frame.h);
|
| 604 |
-
if (APP.ui.overlays.renderFrameOverlay) {
|
| 605 |
-
APP.ui.overlays.renderFrameOverlay();
|
| 606 |
-
}
|
| 607 |
-
}
|
| 608 |
-
};
|
| 609 |
-
img.onerror = () => {
|
| 610 |
-
// Fallback to processed or raw first frame
|
| 611 |
-
APP.core.video.displayProcessedFirstFrame();
|
| 612 |
-
};
|
| 613 |
-
img.src = state.hf.depthFirstFrameUrl;
|
| 614 |
-
} else if (state.hf.firstFrameUrl) {
|
| 615 |
-
// Show processed first frame
|
| 616 |
-
APP.core.video.displayProcessedFirstFrame();
|
| 617 |
-
} else {
|
| 618 |
-
// Fallback to local video first frame
|
| 619 |
-
APP.core.video.drawFirstFrame();
|
| 620 |
-
}
|
| 621 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/init.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
// Initialize Global Namespace
|
| 2 |
-
window.APP = {
|
| 3 |
-
core: {},
|
| 4 |
-
ui: {},
|
| 5 |
-
api: {}
|
| 6 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/main.js
DELETED
|
@@ -1,785 +0,0 @@
|
|
| 1 |
-
// Main Entry Point - Wire up all event handlers and run the application
|
| 2 |
-
document.addEventListener("DOMContentLoaded", () => {
|
| 3 |
-
// Shortcuts
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { $, $$ } = APP.core.utils;
|
| 6 |
-
const { log, setStatus, setHfStatus } = APP.ui.logging;
|
| 7 |
-
const { hfDetectAsync, checkJobStatus, cancelBackendJob, pollAsyncJob } = APP.api.client;
|
| 8 |
-
|
| 9 |
-
// Core modules
|
| 10 |
-
const { captureFirstFrame, drawFirstFrame, unloadVideo, toggleDepthView, toggleFirstFrameDepthView, toggleProcessedFeed, resizeOverlays, setStreamingMode, stopStreamingMode, displayProcessedFirstFrame } = APP.core.video;
|
| 11 |
-
const { syncKnobDisplays, recomputeHEL } = APP.core.hel;
|
| 12 |
-
const { load: loadDemo, getFrameData: getDemoFrameData, enable: enableDemo } = APP.core.demo;
|
| 13 |
-
|
| 14 |
-
// UI Renderers
|
| 15 |
-
const { renderFrameOverlay, renderEngageOverlay, initClickHandler } = APP.ui.overlays;
|
| 16 |
-
const { renderFrameTrackList } = APP.ui.cards;
|
| 17 |
-
const { tickAgentCursor, moveCursorToRect } = APP.ui.cursor;
|
| 18 |
-
const { matchAndUpdateTracks, predictTracks } = APP.core.tracker;
|
| 19 |
-
const { defaultAimpoint } = APP.core.physics;
|
| 20 |
-
|
| 21 |
-
// DOM Elements
|
| 22 |
-
const videoEngage = $("#videoEngage");
|
| 23 |
-
const videoHidden = $("#videoHidden");
|
| 24 |
-
const videoFile = $("#videoFile");
|
| 25 |
-
const btnReason = $("#btnReason");
|
| 26 |
-
const btnCancelReason = $("#btnCancelReason");
|
| 27 |
-
const btnRecompute = $("#btnRecompute");
|
| 28 |
-
const btnClear = $("#btnClear");
|
| 29 |
-
const btnEject = $("#btnEject");
|
| 30 |
-
const btnEngage = $("#btnEngage");
|
| 31 |
-
const btnReset = $("#btnReset");
|
| 32 |
-
const btnPause = $("#btnPause");
|
| 33 |
-
const btnToggleSidebar = $("#btnToggleSidebar");
|
| 34 |
-
|
| 35 |
-
const detectorSelect = $("#detectorSelect");
|
| 36 |
-
const missionText = $("#missionText");
|
| 37 |
-
const cursorMode = $("#cursorMode");
|
| 38 |
-
const frameCanvas = $("#frameCanvas");
|
| 39 |
-
const frameTrackList = $("#frameTrackList");
|
| 40 |
-
const frameEmpty = $("#frameEmpty");
|
| 41 |
-
const frameNote = $("#frameNote");
|
| 42 |
-
const engageEmpty = $("#engageEmpty");
|
| 43 |
-
const engageNote = $("#engageNote");
|
| 44 |
-
|
| 45 |
-
const chipFeed = $("#chipFeed");
|
| 46 |
-
const chipDepth = $("#chipDepth");
|
| 47 |
-
const chipFrameDepth = $("#chipFrameDepth");
|
| 48 |
-
|
| 49 |
-
// Initialization
|
| 50 |
-
function init() {
|
| 51 |
-
log("System initializing...", "t");
|
| 52 |
-
|
| 53 |
-
setupFileUpload();
|
| 54 |
-
setupControls();
|
| 55 |
-
setupKnobListeners();
|
| 56 |
-
setupChipToggles();
|
| 57 |
-
setupTabSwitching();
|
| 58 |
-
|
| 59 |
-
// Initial UI sync
|
| 60 |
-
syncKnobDisplays();
|
| 61 |
-
setHfStatus("idle");
|
| 62 |
-
|
| 63 |
-
// Enable click-to-select on engage overlay
|
| 64 |
-
initClickHandler();
|
| 65 |
-
|
| 66 |
-
// Start main loop
|
| 67 |
-
requestAnimationFrame(loop);
|
| 68 |
-
|
| 69 |
-
// Load demo data (if available)
|
| 70 |
-
loadDemo().then(() => {
|
| 71 |
-
// hidden usage: enable if video filename matches "demo" or manually
|
| 72 |
-
// APP.core.demo.enable(true);
|
| 73 |
-
});
|
| 74 |
-
|
| 75 |
-
log("System READY.", "g");
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
function setupFileUpload() {
|
| 79 |
-
if (!videoFile) return;
|
| 80 |
-
|
| 81 |
-
videoFile.addEventListener("change", async (e) => {
|
| 82 |
-
const file = e.target.files[0];
|
| 83 |
-
if (!file) return;
|
| 84 |
-
|
| 85 |
-
state.videoFile = file;
|
| 86 |
-
state.videoUrl = URL.createObjectURL(file);
|
| 87 |
-
state.videoLoaded = true;
|
| 88 |
-
|
| 89 |
-
// Show meta
|
| 90 |
-
const videoMeta = $("#videoMeta");
|
| 91 |
-
if (videoMeta) videoMeta.textContent = file.name;
|
| 92 |
-
|
| 93 |
-
// Load video into engage player
|
| 94 |
-
if (videoEngage) {
|
| 95 |
-
videoEngage.src = state.videoUrl;
|
| 96 |
-
videoEngage.load();
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
// Hide empty states
|
| 100 |
-
if (engageEmpty) engageEmpty.style.display = "none";
|
| 101 |
-
|
| 102 |
-
// Capture first frame dimensions (but don't draw - wait for processed frame from backend)
|
| 103 |
-
try {
|
| 104 |
-
await captureFirstFrame();
|
| 105 |
-
// Show placeholder message - actual frame will come from backend
|
| 106 |
-
if (frameNote) frameNote.textContent = "Video loaded (run Detect for processed frame)";
|
| 107 |
-
if (engageNote) engageNote.textContent = "Ready for Track";
|
| 108 |
-
} catch (err) {
|
| 109 |
-
log(`First frame capture failed: ${err.message}`, "e");
|
| 110 |
-
}
|
| 111 |
-
|
| 112 |
-
setStatus("warn", "READY · Video loaded (run Detect)");
|
| 113 |
-
log(`Video loaded: ${file.name}`, "g");
|
| 114 |
-
|
| 115 |
-
// Load video-specific demo tracks (e.g., helicopter demo)
|
| 116 |
-
if (APP.core.demo.loadForVideo) {
|
| 117 |
-
await APP.core.demo.loadForVideo(file.name);
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
// Auto-enable demo mode if filename contains "demo" or helicopter video
|
| 121 |
-
const shouldEnableDemo = file.name.toLowerCase().includes("demo") ||
|
| 122 |
-
file.name.toLowerCase().includes("enhance_video_movement");
|
| 123 |
-
if (shouldEnableDemo && APP.core.demo.data) {
|
| 124 |
-
enableDemo(true);
|
| 125 |
-
log("Auto-enabled DEMO mode for this video.", "g");
|
| 126 |
-
}
|
| 127 |
-
});
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
function setupControls() {
|
| 131 |
-
// Reason button
|
| 132 |
-
if (btnReason) {
|
| 133 |
-
btnReason.addEventListener("click", runReason);
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
// Cancel Reason button
|
| 137 |
-
if (btnCancelReason) {
|
| 138 |
-
btnCancelReason.addEventListener("click", cancelReasoning);
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
// Recompute HEL button
|
| 142 |
-
if (btnRecompute) {
|
| 143 |
-
btnRecompute.addEventListener("click", async () => {
|
| 144 |
-
if (!state.hasReasoned) return;
|
| 145 |
-
await recomputeHEL();
|
| 146 |
-
renderFrameOverlay();
|
| 147 |
-
|
| 148 |
-
log("Parameters recomputed.", "g");
|
| 149 |
-
});
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
// Clear button
|
| 153 |
-
if (btnClear) {
|
| 154 |
-
btnClear.addEventListener("click", () => {
|
| 155 |
-
state.detections = [];
|
| 156 |
-
state.selectedId = null;
|
| 157 |
-
renderFrameTrackList();
|
| 158 |
-
renderFrameOverlay();
|
| 159 |
-
|
| 160 |
-
log("Detections cleared.", "t");
|
| 161 |
-
});
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
// Eject button
|
| 165 |
-
if (btnEject) {
|
| 166 |
-
btnEject.addEventListener("click", async () => {
|
| 167 |
-
await unloadVideo();
|
| 168 |
-
});
|
| 169 |
-
}
|
| 170 |
-
|
| 171 |
-
// Engage button
|
| 172 |
-
if (btnEngage) {
|
| 173 |
-
btnEngage.addEventListener("click", runEngage);
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
// Pause button
|
| 177 |
-
if (btnPause) {
|
| 178 |
-
btnPause.addEventListener("click", () => {
|
| 179 |
-
if (videoEngage) videoEngage.pause();
|
| 180 |
-
state.tracker.running = false;
|
| 181 |
-
log("Tracking paused.", "t");
|
| 182 |
-
});
|
| 183 |
-
}
|
| 184 |
-
|
| 185 |
-
// Reset button
|
| 186 |
-
if (btnReset) {
|
| 187 |
-
btnReset.addEventListener("click", () => {
|
| 188 |
-
if (videoEngage) {
|
| 189 |
-
videoEngage.pause();
|
| 190 |
-
videoEngage.currentTime = 0;
|
| 191 |
-
}
|
| 192 |
-
state.tracker.tracks = [];
|
| 193 |
-
state.tracker.running = false;
|
| 194 |
-
state.tracker.nextId = 1;
|
| 195 |
-
renderFrameTrackList();
|
| 196 |
-
log("Tracking reset.", "t");
|
| 197 |
-
});
|
| 198 |
-
}
|
| 199 |
-
|
| 200 |
-
// Sidebar toggle (Tab 2)
|
| 201 |
-
if (btnToggleSidebar) {
|
| 202 |
-
btnToggleSidebar.addEventListener("click", () => {
|
| 203 |
-
const engageGrid = $(".engage-grid");
|
| 204 |
-
if (engageGrid) {
|
| 205 |
-
engageGrid.classList.toggle("sidebar-collapsed");
|
| 206 |
-
btnToggleSidebar.textContent = engageGrid.classList.contains("sidebar-collapsed")
|
| 207 |
-
? "▶ Show Sidebar"
|
| 208 |
-
: "◀ Hide Sidebar";
|
| 209 |
-
}
|
| 210 |
-
});
|
| 211 |
-
}
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
// Track selection event — re-renders cards (with inline features for active card)
|
| 217 |
-
document.addEventListener("track-selected", (e) => {
|
| 218 |
-
state.selectedId = e.detail.id;
|
| 219 |
-
state.tracker.selectedTrackId = e.detail.id;
|
| 220 |
-
renderFrameTrackList();
|
| 221 |
-
renderFrameOverlay();
|
| 222 |
-
renderEngageOverlay();
|
| 223 |
-
|
| 224 |
-
// Scroll selected card into view (Tab 2 sidebar)
|
| 225 |
-
const card = document.getElementById(`card-${e.detail.id}`);
|
| 226 |
-
if (card) card.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
| 227 |
-
});
|
| 228 |
-
|
| 229 |
-
// Cursor mode toggle
|
| 230 |
-
if (cursorMode) {
|
| 231 |
-
cursorMode.addEventListener("change", () => {
|
| 232 |
-
state.ui.cursorMode = cursorMode.value;
|
| 233 |
-
if (state.ui.cursorMode === "off" && APP.ui.cursor.setCursorVisible) {
|
| 234 |
-
APP.ui.cursor.setCursorVisible(false);
|
| 235 |
-
}
|
| 236 |
-
});
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
function setupKnobListeners() {
|
| 240 |
-
// Listen to all inputs and selects for knob updates
|
| 241 |
-
const inputs = Array.from(document.querySelectorAll("input, select"));
|
| 242 |
-
inputs.forEach(el => {
|
| 243 |
-
el.addEventListener("input", () => {
|
| 244 |
-
syncKnobDisplays();
|
| 245 |
-
if (state.hasReasoned) {
|
| 246 |
-
recomputeHEL();
|
| 247 |
-
renderFrameOverlay();
|
| 248 |
-
|
| 249 |
-
}
|
| 250 |
-
});
|
| 251 |
-
});
|
| 252 |
-
|
| 253 |
-
// Initial sync
|
| 254 |
-
syncKnobDisplays();
|
| 255 |
-
}
|
| 256 |
-
|
| 257 |
-
function setupChipToggles() {
|
| 258 |
-
// Toggle processed/raw feed
|
| 259 |
-
if (chipFeed) {
|
| 260 |
-
chipFeed.style.cursor = "pointer";
|
| 261 |
-
chipFeed.addEventListener("click", () => {
|
| 262 |
-
if (!state.videoLoaded) return;
|
| 263 |
-
toggleProcessedFeed();
|
| 264 |
-
log(`Feed set to: ${state.useProcessedFeed ? "HF" : "RAW"}`, "t");
|
| 265 |
-
});
|
| 266 |
-
}
|
| 267 |
-
|
| 268 |
-
// Toggle depth view (Tab 2)
|
| 269 |
-
if (chipDepth) {
|
| 270 |
-
chipDepth.style.cursor = "pointer";
|
| 271 |
-
chipDepth.addEventListener("click", () => {
|
| 272 |
-
if (!state.videoLoaded) return;
|
| 273 |
-
toggleDepthView();
|
| 274 |
-
log(`Engage view set to: ${state.useDepthFeed ? "DEPTH" : "DEFAULT"}`, "t");
|
| 275 |
-
});
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
// Toggle first frame depth view (Tab 1)
|
| 279 |
-
if (chipFrameDepth) {
|
| 280 |
-
chipFrameDepth.style.cursor = "pointer";
|
| 281 |
-
chipFrameDepth.addEventListener("click", () => {
|
| 282 |
-
if (!state.videoLoaded) return;
|
| 283 |
-
if (!state.hf.depthFirstFrameUrl) {
|
| 284 |
-
log("First frame depth not ready yet. Run Detect and wait for depth processing.", "w");
|
| 285 |
-
return;
|
| 286 |
-
}
|
| 287 |
-
toggleFirstFrameDepthView();
|
| 288 |
-
log(`First frame view set to: ${state.useFrameDepthView ? "DEPTH" : "DEFAULT"}`, "t");
|
| 289 |
-
});
|
| 290 |
-
}
|
| 291 |
-
}
|
| 292 |
-
|
| 293 |
-
function setupTabSwitching() {
|
| 294 |
-
const tabs = Array.from(document.querySelectorAll(".tabbtn"));
|
| 295 |
-
tabs.forEach(btn => {
|
| 296 |
-
btn.addEventListener("click", () => {
|
| 297 |
-
tabs.forEach(b => b.classList.remove("active"));
|
| 298 |
-
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
|
| 299 |
-
btn.classList.add("active");
|
| 300 |
-
const tabId = `#tab-${btn.dataset.tab}`;
|
| 301 |
-
const tab = $(tabId);
|
| 302 |
-
if (tab) tab.classList.add("active");
|
| 303 |
-
|
| 304 |
-
// Tab-specific actions
|
| 305 |
-
if (btn.dataset.tab === "engage") {
|
| 306 |
-
resizeOverlays();
|
| 307 |
-
}
|
| 308 |
-
});
|
| 309 |
-
});
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
async function runReason() {
|
| 313 |
-
if (!state.videoLoaded) {
|
| 314 |
-
log("No video loaded. Upload a video first.", "w");
|
| 315 |
-
setStatus("warn", "READY · Upload a video");
|
| 316 |
-
return;
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
if (state.isReasoning) {
|
| 320 |
-
log("Detection already in progress. Please wait.", "w");
|
| 321 |
-
return;
|
| 322 |
-
}
|
| 323 |
-
|
| 324 |
-
// Lock the Reason process
|
| 325 |
-
state.isReasoning = true;
|
| 326 |
-
if (btnReason) {
|
| 327 |
-
btnReason.disabled = true;
|
| 328 |
-
btnReason.style.opacity = "0.5";
|
| 329 |
-
btnReason.style.cursor = "not-allowed";
|
| 330 |
-
}
|
| 331 |
-
if (btnCancelReason) btnCancelReason.style.display = "inline-block";
|
| 332 |
-
if (btnEngage) btnEngage.disabled = true;
|
| 333 |
-
|
| 334 |
-
// Clear previous detections
|
| 335 |
-
state.detections = [];
|
| 336 |
-
state.selectedId = null;
|
| 337 |
-
renderFrameTrackList();
|
| 338 |
-
renderFrameOverlay();
|
| 339 |
-
|
| 340 |
-
setStatus("warn", "DETECTING · Running perception pipeline");
|
| 341 |
-
|
| 342 |
-
// Agent cursor flair
|
| 343 |
-
if (state.ui.cursorMode === "on" && moveCursorToRect) {
|
| 344 |
-
if (btnReason) moveCursorToRect(btnReason.getBoundingClientRect());
|
| 345 |
-
if (frameCanvas) setTimeout(() => moveCursorToRect(frameCanvas.getBoundingClientRect()), 260);
|
| 346 |
-
if (frameTrackList) setTimeout(() => moveCursorToRect(frameTrackList.getBoundingClientRect()), 560);
|
| 347 |
-
}
|
| 348 |
-
|
| 349 |
-
try {
|
| 350 |
-
const selectedOption = detectorSelect ? detectorSelect.options[detectorSelect.selectedIndex] : null;
|
| 351 |
-
const selectedValue = detectorSelect ? detectorSelect.value : "yolo11";
|
| 352 |
-
const kind = selectedOption ? selectedOption.getAttribute("data-kind") : "object";
|
| 353 |
-
const queries = missionText ? missionText.value.trim() : "";
|
| 354 |
-
const enableGPT = $("#enableGPTToggle")?.checked || false;
|
| 355 |
-
const enableDepth = false; // depth mode disabled
|
| 356 |
-
|
| 357 |
-
// Determine mode and model parameter from data-kind attribute
|
| 358 |
-
let mode, detectorParam, segmenterParam;
|
| 359 |
-
if (kind === "segmentation") {
|
| 360 |
-
mode = "segmentation";
|
| 361 |
-
segmenterParam = selectedValue;
|
| 362 |
-
detectorParam = "yolo11"; // default, unused for segmentation
|
| 363 |
-
} else if (kind === "drone") {
|
| 364 |
-
mode = "drone_detection";
|
| 365 |
-
detectorParam = selectedValue;
|
| 366 |
-
segmenterParam = "GSAM2-L";
|
| 367 |
-
} else {
|
| 368 |
-
mode = "object_detection";
|
| 369 |
-
detectorParam = selectedValue;
|
| 370 |
-
segmenterParam = "GSAM2-L";
|
| 371 |
-
}
|
| 372 |
-
|
| 373 |
-
const form = new FormData();
|
| 374 |
-
form.append("video", state.videoFile);
|
| 375 |
-
form.append("mode", mode);
|
| 376 |
-
if (queries) form.append("queries", queries);
|
| 377 |
-
form.append("detector", detectorParam);
|
| 378 |
-
form.append("segmenter", segmenterParam);
|
| 379 |
-
form.append("enable_gpt", enableGPT ? "true" : "false");
|
| 380 |
-
form.append("enable_depth", enableDepth ? "true" : "false");
|
| 381 |
-
|
| 382 |
-
log(`Submitting job to ${state.hf.baseUrl}...`, "t");
|
| 383 |
-
setHfStatus("submitting job...");
|
| 384 |
-
|
| 385 |
-
const data = await hfDetectAsync(form);
|
| 386 |
-
|
| 387 |
-
state.hf.asyncJobId = data.job_id;
|
| 388 |
-
|
| 389 |
-
// Store mission specification for chatbot context
|
| 390 |
-
if (data.mission_spec) {
|
| 391 |
-
state.hf.missionSpec = data.mission_spec;
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
// Store raw detections (will process after image loads to get correct dimensions)
|
| 395 |
-
const rawDetections = data.first_frame_detections || [];
|
| 396 |
-
|
| 397 |
-
// Display processed first frame from backend (only processed frame, not raw)
|
| 398 |
-
// This is async - image loading will update state.frame.w/h
|
| 399 |
-
if (data.first_frame_url) {
|
| 400 |
-
state.hf.firstFrameUrl = data.first_frame_url.startsWith("http")
|
| 401 |
-
? data.first_frame_url
|
| 402 |
-
: `${state.hf.baseUrl}${data.first_frame_url}`;
|
| 403 |
-
|
| 404 |
-
// Wait for image to load so we have correct dimensions before processing detections
|
| 405 |
-
await new Promise((resolve, reject) => {
|
| 406 |
-
const img = new Image();
|
| 407 |
-
img.crossOrigin = "anonymous";
|
| 408 |
-
img.onload = () => {
|
| 409 |
-
// Update frame dimensions from loaded image
|
| 410 |
-
state.frame.w = img.naturalWidth || 1280;
|
| 411 |
-
state.frame.h = img.naturalHeight || 720;
|
| 412 |
-
|
| 413 |
-
// Resize canvases to match
|
| 414 |
-
const frameCanvas = $("#frameCanvas");
|
| 415 |
-
const frameOverlay = $("#frameOverlay");
|
| 416 |
-
if (frameCanvas) {
|
| 417 |
-
frameCanvas.width = state.frame.w;
|
| 418 |
-
frameCanvas.height = state.frame.h;
|
| 419 |
-
frameCanvas.getContext("2d").drawImage(img, 0, 0, state.frame.w, state.frame.h);
|
| 420 |
-
}
|
| 421 |
-
if (frameOverlay) {
|
| 422 |
-
frameOverlay.width = state.frame.w;
|
| 423 |
-
frameOverlay.height = state.frame.h;
|
| 424 |
-
}
|
| 425 |
-
|
| 426 |
-
// Hide empty state
|
| 427 |
-
const frameEmpty = $("#frameEmpty");
|
| 428 |
-
const frameNote = $("#frameNote");
|
| 429 |
-
if (frameEmpty) frameEmpty.style.display = "none";
|
| 430 |
-
if (frameNote) frameNote.textContent = "Processed (from backend)";
|
| 431 |
-
|
| 432 |
-
log(`Processed first frame displayed (${state.frame.w}×${state.frame.h})`, "g");
|
| 433 |
-
resolve();
|
| 434 |
-
};
|
| 435 |
-
img.onerror = () => {
|
| 436 |
-
log("Failed to load processed first frame, using local frame", "w");
|
| 437 |
-
drawFirstFrame();
|
| 438 |
-
resolve();
|
| 439 |
-
};
|
| 440 |
-
img.src = state.hf.firstFrameUrl;
|
| 441 |
-
});
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
-
// NOW process detections (after frame dimensions are correct)
|
| 445 |
-
if (rawDetections.length > 0) {
|
| 446 |
-
processFirstFrameDetections(rawDetections);
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
// Mark first frame as ready (for radar display)
|
| 450 |
-
state.firstFrameReady = true;
|
| 451 |
-
|
| 452 |
-
// Store depth URLs if provided
|
| 453 |
-
if (data.depth_video_url) {
|
| 454 |
-
state.hf.depthVideoUrl = data.depth_video_url.startsWith("http")
|
| 455 |
-
? data.depth_video_url
|
| 456 |
-
: `${state.hf.baseUrl}${data.depth_video_url}`;
|
| 457 |
-
log("Depth video URL received", "t");
|
| 458 |
-
}
|
| 459 |
-
if (data.first_frame_depth_url) {
|
| 460 |
-
state.hf.depthFirstFrameUrl = data.first_frame_depth_url.startsWith("http")
|
| 461 |
-
? data.first_frame_depth_url
|
| 462 |
-
: `${state.hf.baseUrl}${data.first_frame_depth_url}`;
|
| 463 |
-
log("First frame depth URL received", "t");
|
| 464 |
-
}
|
| 465 |
-
|
| 466 |
-
// Enable streaming mode if stream_url is provided (Tab 2 live view)
|
| 467 |
-
const enableStream = $("#enableStreamToggle")?.checked;
|
| 468 |
-
|
| 469 |
-
if (data.stream_url && enableStream) {
|
| 470 |
-
const streamUrl = data.stream_url.startsWith("http")
|
| 471 |
-
? data.stream_url
|
| 472 |
-
: `${state.hf.baseUrl}${data.stream_url}`;
|
| 473 |
-
log("Activating live stream...", "t");
|
| 474 |
-
setStreamingMode(streamUrl);
|
| 475 |
-
log("Live view available in 'Track' tab.", "g");
|
| 476 |
-
setStatus("warn", "Live processing... View in Track tab");
|
| 477 |
-
|
| 478 |
-
// Trigger resize/render for Tab 2
|
| 479 |
-
resizeOverlays();
|
| 480 |
-
}
|
| 481 |
-
|
| 482 |
-
// Start polling for completion
|
| 483 |
-
pollAsyncJob().then(() => {
|
| 484 |
-
log("Video processing complete.", "g");
|
| 485 |
-
// Stop streaming mode once video is ready
|
| 486 |
-
stopStreamingMode();
|
| 487 |
-
|
| 488 |
-
state.hasReasoned = true;
|
| 489 |
-
setStatus("good", "READY · Detection complete (you can Track)");
|
| 490 |
-
log("Detection complete. Ready to Track.", "g");
|
| 491 |
-
|
| 492 |
-
// Seed tracks for Tab 2
|
| 493 |
-
seedTracksFromTab1();
|
| 494 |
-
|
| 495 |
-
// Re-enable engage button
|
| 496 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 497 |
-
}).catch(err => {
|
| 498 |
-
log(`Polling error: ${err.message}`, "e");
|
| 499 |
-
stopStreamingMode();
|
| 500 |
-
});
|
| 501 |
-
|
| 502 |
-
// Initial status (processing in background)
|
| 503 |
-
setStatus("warn", "PROCESSING · Analysing video...");
|
| 504 |
-
log("Reasoning started (processing in background)...", "t");
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
} catch (err) {
|
| 509 |
-
setStatus("bad", "ERROR · Detection failed");
|
| 510 |
-
log(`Detection failed: ${err.message}`, "e");
|
| 511 |
-
console.error(err);
|
| 512 |
-
} finally {
|
| 513 |
-
state.isReasoning = false;
|
| 514 |
-
if (btnReason) {
|
| 515 |
-
btnReason.disabled = false;
|
| 516 |
-
btnReason.style.opacity = "1";
|
| 517 |
-
btnReason.style.cursor = "pointer";
|
| 518 |
-
}
|
| 519 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 520 |
-
// Re-enable engage button in case of failure
|
| 521 |
-
if (btnEngage) btnEngage.disabled = false;
|
| 522 |
-
}
|
| 523 |
-
}
|
| 524 |
-
|
| 525 |
-
function processFirstFrameDetections(dets) {
|
| 526 |
-
state.detections = dets.map((d, i) => {
|
| 527 |
-
const id = d.track_id || `T${String(i + 1).padStart(2, "0")}`;
|
| 528 |
-
const ap = defaultAimpoint(d.label || d.class);
|
| 529 |
-
const bbox = d.bbox
|
| 530 |
-
? { x: d.bbox[0], y: d.bbox[1], w: d.bbox[2] - d.bbox[0], h: d.bbox[3] - d.bbox[1] }
|
| 531 |
-
: { x: 0, y: 0, w: 10, h: 10 };
|
| 532 |
-
|
| 533 |
-
// Build features from universal schema via canonical mapping
|
| 534 |
-
let features = APP.core.gptMapping.buildFeatures(d.gpt_raw);
|
| 535 |
-
|
| 536 |
-
return {
|
| 537 |
-
id,
|
| 538 |
-
label: d.label || d.class,
|
| 539 |
-
score: d.score || 0.5,
|
| 540 |
-
bbox,
|
| 541 |
-
aim: { ...ap },
|
| 542 |
-
features,
|
| 543 |
-
baseRange_m: null,
|
| 544 |
-
baseAreaFrac: (bbox.w * bbox.h) / (state.frame.w * state.frame.h),
|
| 545 |
-
baseDwell_s: 5.0,
|
| 546 |
-
reqP_kW: 40,
|
| 547 |
-
maxP_kW: 0,
|
| 548 |
-
pkill: 0,
|
| 549 |
-
// Depth fields
|
| 550 |
-
depth_est_m: (d.depth_est_m !== undefined && d.depth_est_m !== null) ? d.depth_est_m : null,
|
| 551 |
-
depth_rel: (d.depth_rel !== undefined && d.depth_rel !== null) ? d.depth_rel : null,
|
| 552 |
-
depth_valid: d.depth_valid ?? false,
|
| 553 |
-
gpt_distance_m: d.gpt_distance_m || null,
|
| 554 |
-
gpt_direction: d.gpt_direction || null,
|
| 555 |
-
gpt_description: d.gpt_description || null,
|
| 556 |
-
// Threat Intelligence
|
| 557 |
-
threat_level_score: d.threat_level_score || 0,
|
| 558 |
-
threat_classification: d.threat_classification || "Unknown",
|
| 559 |
-
weapon_readiness: d.weapon_readiness || "Unknown",
|
| 560 |
-
// Mission relevance and assessment status
|
| 561 |
-
mission_relevant: d.mission_relevant ?? null,
|
| 562 |
-
relevance_reason: d.relevance_reason || null,
|
| 563 |
-
assessment_status: d.assessment_status || APP.core.gptMapping.STATUS.UNASSESSED,
|
| 564 |
-
assessment_frame_index: d.assessment_frame_index ?? null,
|
| 565 |
-
};
|
| 566 |
-
});
|
| 567 |
-
|
| 568 |
-
state.selectedId = state.detections[0]?.id || null;
|
| 569 |
-
|
| 570 |
-
renderFrameTrackList();
|
| 571 |
-
renderFrameOverlay();
|
| 572 |
-
|
| 573 |
-
log(`Detected ${state.detections.length} objects in first frame.`, "g");
|
| 574 |
-
}
|
| 575 |
-
|
| 576 |
-
function seedTracksFromTab1() {
|
| 577 |
-
const rangeBase = $("#rangeBase");
|
| 578 |
-
state.tracker.tracks = state.detections.map(d => ({
|
| 579 |
-
id: d.id,
|
| 580 |
-
label: d.label,
|
| 581 |
-
bbox: { ...d.bbox },
|
| 582 |
-
score: d.score,
|
| 583 |
-
aimRel: d.aim ? { relx: d.aim.relx, rely: d.aim.rely, label: d.aim.label } : { relx: 0.5, rely: 0.5, label: "center_mass" },
|
| 584 |
-
baseAreaFrac: d.baseAreaFrac || ((d.bbox.w * d.bbox.h) / (state.frame.w * state.frame.h)),
|
| 585 |
-
baseRange_m: d.baseRange_m || (rangeBase ? +rangeBase.value : 1500),
|
| 586 |
-
baseDwell_s: d.baseDwell_s || 4.0,
|
| 587 |
-
reqP_kW: d.reqP_kW || 35,
|
| 588 |
-
depth_rel: d.depth_rel,
|
| 589 |
-
depth_est_m: d.depth_est_m,
|
| 590 |
-
depth_valid: d.depth_valid,
|
| 591 |
-
lastDepthBbox: d.depth_valid ? { ...d.bbox } : null,
|
| 592 |
-
gpt_distance_m: d.gpt_distance_m,
|
| 593 |
-
gpt_direction: d.gpt_direction,
|
| 594 |
-
gpt_description: d.gpt_description,
|
| 595 |
-
lastSeen: APP.core.utils.now(),
|
| 596 |
-
vx: 0,
|
| 597 |
-
vy: 0,
|
| 598 |
-
dwellAccum: 0,
|
| 599 |
-
killed: false,
|
| 600 |
-
state: "TRACK",
|
| 601 |
-
assessT: 0
|
| 602 |
-
}));
|
| 603 |
-
state.tracker.nextId = state.detections.length + 1;
|
| 604 |
-
log(`Seeded ${state.tracker.tracks.length} tracks from Tab 1 detections.`, "t");
|
| 605 |
-
}
|
| 606 |
-
|
| 607 |
-
function cancelReasoning() {
|
| 608 |
-
// Stop HF polling if running
|
| 609 |
-
if (state.hf.asyncPollInterval) {
|
| 610 |
-
clearInterval(state.hf.asyncPollInterval);
|
| 611 |
-
state.hf.asyncPollInterval = null;
|
| 612 |
-
log("HF polling stopped.", "w");
|
| 613 |
-
}
|
| 614 |
-
|
| 615 |
-
// Stop streaming mode
|
| 616 |
-
stopStreamingMode();
|
| 617 |
-
|
| 618 |
-
// Cancel backend job if it exists
|
| 619 |
-
const jobId = state.hf.asyncJobId;
|
| 620 |
-
if (jobId) {
|
| 621 |
-
cancelBackendJob(jobId, "cancel button");
|
| 622 |
-
}
|
| 623 |
-
|
| 624 |
-
// Reset state
|
| 625 |
-
state.isReasoning = false;
|
| 626 |
-
state.hf.busy = false;
|
| 627 |
-
state.hf.asyncJobId = null;
|
| 628 |
-
state.hf.completedJobId = null;
|
| 629 |
-
state.hf.asyncStatus = "cancelled";
|
| 630 |
-
|
| 631 |
-
// Re-enable Reason button
|
| 632 |
-
if (btnReason) {
|
| 633 |
-
btnReason.disabled = false;
|
| 634 |
-
btnReason.style.opacity = "1";
|
| 635 |
-
btnReason.style.cursor = "pointer";
|
| 636 |
-
}
|
| 637 |
-
if (btnCancelReason) btnCancelReason.style.display = "none";
|
| 638 |
-
|
| 639 |
-
setStatus("warn", "CANCELLED · Detection stopped");
|
| 640 |
-
setHfStatus("cancelled (stopped by user)");
|
| 641 |
-
log("Detection cancelled by user.", "w");
|
| 642 |
-
}
|
| 643 |
-
|
| 644 |
-
function runEngage() {
|
| 645 |
-
if (!state.hasReasoned) {
|
| 646 |
-
log("Please run Detect first.", "w");
|
| 647 |
-
return;
|
| 648 |
-
}
|
| 649 |
-
|
| 650 |
-
if (state.hf.asyncJobId) {
|
| 651 |
-
log("Processing still in progress. Please wait.", "w");
|
| 652 |
-
// If we are streaming, make sure we are on the engage tab to see it
|
| 653 |
-
const engageTab = $(`.tabbtn[data-tab="engage"]`);
|
| 654 |
-
if (engageTab) engageTab.click();
|
| 655 |
-
return;
|
| 656 |
-
}
|
| 657 |
-
|
| 658 |
-
// Switch to engage tab
|
| 659 |
-
const engageTab = $(`.tabbtn[data-tab="engage"]`);
|
| 660 |
-
if (engageTab) engageTab.click();
|
| 661 |
-
|
| 662 |
-
// Set video source
|
| 663 |
-
if (videoEngage) {
|
| 664 |
-
videoEngage.src = state.hf.processedUrl || state.videoUrl;
|
| 665 |
-
videoEngage.play().catch(err => {
|
| 666 |
-
log(`Video playback failed: ${err.message}`, "e");
|
| 667 |
-
});
|
| 668 |
-
}
|
| 669 |
-
|
| 670 |
-
state.tracker.running = true;
|
| 671 |
-
state.tracker.lastFrameTime = APP.core.utils.now();
|
| 672 |
-
|
| 673 |
-
// Ensure tracks are seeded
|
| 674 |
-
if (state.tracker.tracks.length === 0) {
|
| 675 |
-
seedTracksFromTab1();
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
log("Tracking started.", "g");
|
| 679 |
-
}
|
| 680 |
-
|
| 681 |
-
function loop() {
|
| 682 |
-
const { now } = APP.core.utils;
|
| 683 |
-
const t = now();
|
| 684 |
-
|
| 685 |
-
// Guard against huge dt on first frame
|
| 686 |
-
if (state.tracker.lastFrameTime === 0) state.tracker.lastFrameTime = t;
|
| 687 |
-
|
| 688 |
-
const dt = Math.min((t - state.tracker.lastFrameTime) / 1000, 0.1);
|
| 689 |
-
state.tracker.lastFrameTime = t;
|
| 690 |
-
|
| 691 |
-
// ── Always keep track positions fresh (playing OR paused) ──
|
| 692 |
-
// This ensures bboxes remain clickable regardless of playback state.
|
| 693 |
-
if (state.tracker.running && videoEngage && state.tracker.tracks.length > 0) {
|
| 694 |
-
if (APP.core.demo.active && APP.core.demo.data) {
|
| 695 |
-
// DEMO MODE: sync tracks to current video time (even when paused)
|
| 696 |
-
const demoTracks = getDemoFrameData(videoEngage.currentTime);
|
| 697 |
-
if (demoTracks) {
|
| 698 |
-
const tracksClone = JSON.parse(JSON.stringify(demoTracks));
|
| 699 |
-
|
| 700 |
-
state.tracker.tracks = tracksClone.map(d => ({
|
| 701 |
-
...d,
|
| 702 |
-
lastSeen: t,
|
| 703 |
-
state: "TRACK",
|
| 704 |
-
depth_valid: true,
|
| 705 |
-
depth_est_m: d.gpt_distance_m || 1000,
|
| 706 |
-
}));
|
| 707 |
-
|
| 708 |
-
const w = videoEngage.videoWidth || state.frame.w || 1280;
|
| 709 |
-
const h = videoEngage.videoHeight || state.frame.h || 720;
|
| 710 |
-
|
| 711 |
-
state.tracker.tracks.forEach(tr => {
|
| 712 |
-
if (tr.bbox.x > 1 || tr.bbox.w > 1) {
|
| 713 |
-
tr.bbox.x /= w;
|
| 714 |
-
tr.bbox.y /= h;
|
| 715 |
-
tr.bbox.w /= w;
|
| 716 |
-
tr.bbox.h /= h;
|
| 717 |
-
}
|
| 718 |
-
});
|
| 719 |
-
}
|
| 720 |
-
} else {
|
| 721 |
-
// NORMAL MODE: predict positions every frame
|
| 722 |
-
predictTracks(dt);
|
| 723 |
-
|
| 724 |
-
// Backend sync every 333ms (works while paused too)
|
| 725 |
-
const jobId = state.hf.asyncJobId || state.hf.completedJobId;
|
| 726 |
-
if (jobId && (t - state.tracker.lastHFSync > 333)) {
|
| 727 |
-
const frameIdx = Math.floor(videoEngage.currentTime * 30);
|
| 728 |
-
APP.core.tracker.syncWithBackend(frameIdx);
|
| 729 |
-
state.tracker.lastHFSync = t;
|
| 730 |
-
}
|
| 731 |
-
}
|
| 732 |
-
}
|
| 733 |
-
|
| 734 |
-
// ── Card rendering & GPT analysis: only during active playback ──
|
| 735 |
-
if (state.tracker.running && videoEngage && !videoEngage.paused) {
|
| 736 |
-
state.tracker.frameCount++;
|
| 737 |
-
|
| 738 |
-
const framesSinceRender = state.tracker.frameCount - state.tracker._lastCardRenderFrame;
|
| 739 |
-
if (state.tracker._newObjectDetected || framesSinceRender >= 40) {
|
| 740 |
-
renderFrameTrackList();
|
| 741 |
-
state.tracker._lastCardRenderFrame = state.tracker.frameCount;
|
| 742 |
-
state.tracker._newObjectDetected = false;
|
| 743 |
-
|
| 744 |
-
if (!state.tracker._gptBusy && state.tracker.tracks.length > 0) {
|
| 745 |
-
state.tracker._gptBusy = true;
|
| 746 |
-
APP.api.client.analyzeFrame(videoEngage, state.tracker.tracks)
|
| 747 |
-
.then(enriched => {
|
| 748 |
-
for (const rd of enriched) {
|
| 749 |
-
const tid = rd.track_id || rd.id;
|
| 750 |
-
const existing = (state.detections || []).find(d => d.id === tid);
|
| 751 |
-
if (existing && rd.gpt_raw) {
|
| 752 |
-
existing.gpt_raw = rd.gpt_raw;
|
| 753 |
-
existing.features = APP.core.gptMapping.buildFeatures(rd.gpt_raw);
|
| 754 |
-
existing.assessment_status = rd.assessment_status || "ASSESSED";
|
| 755 |
-
existing.threat_level_score = rd.threat_level_score || 0;
|
| 756 |
-
existing.gpt_description = rd.gpt_description || existing.gpt_description;
|
| 757 |
-
existing.gpt_distance_m = rd.gpt_distance_m || existing.gpt_distance_m;
|
| 758 |
-
existing.gpt_direction = rd.gpt_direction || existing.gpt_direction;
|
| 759 |
-
}
|
| 760 |
-
}
|
| 761 |
-
renderFrameTrackList();
|
| 762 |
-
state.tracker._gptBusy = false;
|
| 763 |
-
})
|
| 764 |
-
.catch(err => {
|
| 765 |
-
console.warn("Frame GPT analysis failed:", err);
|
| 766 |
-
state.tracker._gptBusy = false;
|
| 767 |
-
});
|
| 768 |
-
}
|
| 769 |
-
}
|
| 770 |
-
}
|
| 771 |
-
|
| 772 |
-
// Render UI
|
| 773 |
-
if (renderFrameOverlay) renderFrameOverlay();
|
| 774 |
-
if (renderEngageOverlay) renderEngageOverlay();
|
| 775 |
-
if (tickAgentCursor) tickAgentCursor();
|
| 776 |
-
|
| 777 |
-
requestAnimationFrame(loop);
|
| 778 |
-
}
|
| 779 |
-
|
| 780 |
-
// Expose state for debugging
|
| 781 |
-
window.__LP_STATE__ = state;
|
| 782 |
-
|
| 783 |
-
// Start
|
| 784 |
-
init();
|
| 785 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/cards.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
| 1 |
-
APP.ui.cards = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.cards.renderFrameTrackList = function () {
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { $ } = APP.core.utils;
|
| 6 |
-
const frameTrackList = $("#frameTrackList");
|
| 7 |
-
const trackList = $("#trackList"); // Tab 2 (Engage) uses the same card logic
|
| 8 |
-
const trackCount = $("#trackCount");
|
| 9 |
-
|
| 10 |
-
if (!frameTrackList && !trackList) return;
|
| 11 |
-
if (frameTrackList) frameTrackList.innerHTML = "";
|
| 12 |
-
if (trackList) trackList.innerHTML = "";
|
| 13 |
-
|
| 14 |
-
// Filter: only show mission-relevant detections (or all in LEGACY mode)
|
| 15 |
-
const dets = (state.detections || []).filter(d => {
|
| 16 |
-
if (d.mission_relevant === null || d.mission_relevant === undefined) return true;
|
| 17 |
-
return d.mission_relevant === true;
|
| 18 |
-
});
|
| 19 |
-
|
| 20 |
-
if (trackCount) trackCount.textContent = dets.length;
|
| 21 |
-
|
| 22 |
-
if (dets.length === 0) {
|
| 23 |
-
const emptyMsg = '<div style="font-style:italic; color:var(--faint); text-align:center; margin-top:20px; font-size:12px;">No objects tracked.</div>';
|
| 24 |
-
if (frameTrackList) frameTrackList.innerHTML = emptyMsg;
|
| 25 |
-
if (trackList) trackList.innerHTML = emptyMsg;
|
| 26 |
-
return;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
// Sort: ASSESSED first (by threat score), then UNASSESSED, then STALE
|
| 30 |
-
const S = APP.core.gptMapping.STATUS;
|
| 31 |
-
const statusOrder = { [S.ASSESSED]: 0, [S.UNASSESSED]: 1, [S.STALE]: 2 };
|
| 32 |
-
const sorted = [...dets].sort((a, b) => {
|
| 33 |
-
const statusA = statusOrder[a.assessment_status] ?? 1;
|
| 34 |
-
const statusB = statusOrder[b.assessment_status] ?? 1;
|
| 35 |
-
if (statusA !== statusB) return statusA - statusB;
|
| 36 |
-
const scoreA = a.threat_level_score || 0;
|
| 37 |
-
const scoreB = b.threat_level_score || 0;
|
| 38 |
-
if (scoreB !== scoreA) return scoreB - scoreA;
|
| 39 |
-
return (b.score || 0) - (a.score || 0);
|
| 40 |
-
});
|
| 41 |
-
|
| 42 |
-
sorted.forEach((det, i) => {
|
| 43 |
-
const id = det.id || `T${String(i + 1).padStart(2, '0')}`;
|
| 44 |
-
const isActive = state.selectedId === id;
|
| 45 |
-
|
| 46 |
-
let rangeStr = "---";
|
| 47 |
-
let bearingStr = "---";
|
| 48 |
-
|
| 49 |
-
if (det.depth_valid && det.depth_est_m != null) {
|
| 50 |
-
rangeStr = `${Math.round(det.depth_est_m)}m`;
|
| 51 |
-
} else if (det.gpt_distance_m) {
|
| 52 |
-
rangeStr = `~${det.gpt_distance_m}m`;
|
| 53 |
-
} else if (det.baseRange_m) {
|
| 54 |
-
rangeStr = `${Math.round(det.baseRange_m)}m`;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
if (det.gpt_direction) {
|
| 58 |
-
bearingStr = det.gpt_direction;
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
const card = document.createElement("div");
|
| 62 |
-
card.className = "track-card" + (isActive ? " active" : "");
|
| 63 |
-
card.id = `card-${id}`;
|
| 64 |
-
|
| 65 |
-
card.onclick = () => {
|
| 66 |
-
const ev = new CustomEvent("track-selected", { detail: { id } });
|
| 67 |
-
document.dispatchEvent(ev);
|
| 68 |
-
};
|
| 69 |
-
|
| 70 |
-
// Assessment status badge
|
| 71 |
-
let statusBadge = "";
|
| 72 |
-
const assessStatus = det.assessment_status || S.UNASSESSED;
|
| 73 |
-
if (assessStatus === S.UNASSESSED) {
|
| 74 |
-
statusBadge = '<span class="badgemini" style="background:#6c757d; color:white">UNASSESSED</span>';
|
| 75 |
-
} else if (assessStatus === S.STALE) {
|
| 76 |
-
statusBadge = '<span class="badgemini" style="background:#ffc107; color:#333">STALE</span>';
|
| 77 |
-
} else if (det.threat_level_score > 0) {
|
| 78 |
-
statusBadge = `<span class="badgemini" style="background:${det.threat_level_score >= 8 ? '#ff4d4d' : '#ff9f43'}; color:white">T-${det.threat_level_score}</span>`;
|
| 79 |
-
} else if (assessStatus === S.ASSESSED) {
|
| 80 |
-
statusBadge = '<span class="badgemini" style="background:#17a2b8; color:white">ASSESSED</span>';
|
| 81 |
-
}
|
| 82 |
-
|
| 83 |
-
// GPT description (collapsed summary)
|
| 84 |
-
const desc = det.gpt_description
|
| 85 |
-
? `<div class="track-card-body"><span class="gpt-text">${det.gpt_description}</span></div>`
|
| 86 |
-
: "";
|
| 87 |
-
|
| 88 |
-
// Inline features (only shown when active/expanded)
|
| 89 |
-
let featuresHtml = "";
|
| 90 |
-
if (isActive && det.features && Object.keys(det.features).length > 0) {
|
| 91 |
-
const entries = Object.entries(det.features).slice(0, 12);
|
| 92 |
-
const rows = entries.map(([k, v]) =>
|
| 93 |
-
`<div class="feat-row"><span class="feat-key">${k}</span><span class="feat-val">${String(v)}</span></div>`
|
| 94 |
-
).join("");
|
| 95 |
-
featuresHtml = `<div class="track-card-features">${rows}</div>`;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
card.innerHTML = `
|
| 99 |
-
<div class="track-card-header">
|
| 100 |
-
<span>${id} · ${det.label}</span>
|
| 101 |
-
<div style="display:flex; gap:4px; align-items:center">
|
| 102 |
-
${statusBadge}
|
| 103 |
-
${det.mission_relevant === true
|
| 104 |
-
? '<span class="badgemini" style="background:#28a745; color:white">RELEVANT</span>'
|
| 105 |
-
: det.mission_relevant === false
|
| 106 |
-
? '<span class="badgemini" style="background:#6c757d; color:white">N/R</span>'
|
| 107 |
-
: `<span class="badgemini" style="background:rgba(255,255,255,.08); color:rgba(255,255,255,.7)">${(det.score * 100).toFixed(0)}%</span>`
|
| 108 |
-
}
|
| 109 |
-
</div>
|
| 110 |
-
</div>
|
| 111 |
-
<div class="track-card-meta">
|
| 112 |
-
RNG ${rangeStr} · BRG ${bearingStr}
|
| 113 |
-
</div>
|
| 114 |
-
${desc}
|
| 115 |
-
${featuresHtml}
|
| 116 |
-
`;
|
| 117 |
-
if (frameTrackList) frameTrackList.appendChild(card);
|
| 118 |
-
if (trackList) trackList.appendChild(card.cloneNode(true));
|
| 119 |
-
});
|
| 120 |
-
|
| 121 |
-
// Wire up click handlers on cloned Tab 2 cards
|
| 122 |
-
if (trackList) {
|
| 123 |
-
trackList.querySelectorAll(".track-card").forEach(card => {
|
| 124 |
-
const id = card.id.replace("card-", "");
|
| 125 |
-
card.onclick = () => {
|
| 126 |
-
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id } }));
|
| 127 |
-
};
|
| 128 |
-
});
|
| 129 |
-
}
|
| 130 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/chat.js
DELETED
|
@@ -1,155 +0,0 @@
|
|
| 1 |
-
// Chat UI Module - Threat-aware chat with GPT
|
| 2 |
-
(function () {
|
| 3 |
-
const { state } = APP.core;
|
| 4 |
-
const { $ } = APP.core.utils;
|
| 5 |
-
const { log } = APP.ui.logging;
|
| 6 |
-
|
| 7 |
-
let chatHistory = [];
|
| 8 |
-
|
| 9 |
-
/**
|
| 10 |
-
* Initialize the chat module.
|
| 11 |
-
*/
|
| 12 |
-
function init() {
|
| 13 |
-
const chatInput = $("#chatInput");
|
| 14 |
-
const chatSend = $("#chatSend");
|
| 15 |
-
|
| 16 |
-
if (chatSend) {
|
| 17 |
-
chatSend.addEventListener("click", sendMessage);
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
if (chatInput) {
|
| 21 |
-
chatInput.addEventListener("keydown", (e) => {
|
| 22 |
-
if (e.key === "Enter" && !e.shiftKey) {
|
| 23 |
-
e.preventDefault();
|
| 24 |
-
sendMessage();
|
| 25 |
-
}
|
| 26 |
-
});
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
// Toggle chat panel
|
| 30 |
-
const chatToggle = $("#chatToggle");
|
| 31 |
-
const chatPanel = $("#chatPanel");
|
| 32 |
-
if (chatToggle && chatPanel) {
|
| 33 |
-
chatToggle.addEventListener("click", () => {
|
| 34 |
-
chatPanel.classList.toggle("collapsed");
|
| 35 |
-
chatToggle.textContent = chatPanel.classList.contains("collapsed")
|
| 36 |
-
? "▼ Chat"
|
| 37 |
-
: "▲ Close Chat";
|
| 38 |
-
});
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
/**
|
| 43 |
-
* Send a chat message about current threats.
|
| 44 |
-
*/
|
| 45 |
-
async function sendMessage() {
|
| 46 |
-
const chatInput = $("#chatInput");
|
| 47 |
-
const chatMessages = $("#chatMessages");
|
| 48 |
-
|
| 49 |
-
if (!chatInput || !chatMessages) return;
|
| 50 |
-
|
| 51 |
-
const question = chatInput.value.trim();
|
| 52 |
-
if (!question) {
|
| 53 |
-
log("Please enter a question.", "w");
|
| 54 |
-
return;
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
// Check if we have detections
|
| 58 |
-
if (!state.detections || state.detections.length === 0) {
|
| 59 |
-
appendMessage("system", "No detections yet. Run Detect first to analyze the scene.");
|
| 60 |
-
return;
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
// Add user message to UI
|
| 64 |
-
appendMessage("user", question);
|
| 65 |
-
chatInput.value = "";
|
| 66 |
-
chatInput.disabled = true;
|
| 67 |
-
|
| 68 |
-
// Show loading indicator
|
| 69 |
-
const loadingId = appendMessage("assistant", "Analyzing scene...", true);
|
| 70 |
-
|
| 71 |
-
try {
|
| 72 |
-
const response = await APP.api.client.chatAboutThreats(question, state.detections);
|
| 73 |
-
|
| 74 |
-
// Remove loading message
|
| 75 |
-
removeMessage(loadingId);
|
| 76 |
-
|
| 77 |
-
if (response.response) {
|
| 78 |
-
appendMessage("assistant", response.response);
|
| 79 |
-
chatHistory.push({ role: "user", content: question });
|
| 80 |
-
chatHistory.push({ role: "assistant", content: response.response });
|
| 81 |
-
} else if (response.error || response.detail) {
|
| 82 |
-
appendMessage("system", `Error: ${response.error || response.detail}`);
|
| 83 |
-
}
|
| 84 |
-
} catch (err) {
|
| 85 |
-
removeMessage(loadingId);
|
| 86 |
-
appendMessage("system", `Failed to get response: ${err.message}`);
|
| 87 |
-
log(`Chat error: ${err.message}`, "e");
|
| 88 |
-
} finally {
|
| 89 |
-
chatInput.disabled = false;
|
| 90 |
-
chatInput.focus();
|
| 91 |
-
}
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
/**
|
| 95 |
-
* Append a message to the chat display.
|
| 96 |
-
*/
|
| 97 |
-
function appendMessage(role, content, isLoading = false) {
|
| 98 |
-
const chatMessages = $("#chatMessages");
|
| 99 |
-
if (!chatMessages) return null;
|
| 100 |
-
|
| 101 |
-
const msgId = `msg-${Date.now()}`;
|
| 102 |
-
const msgDiv = document.createElement("div");
|
| 103 |
-
msgDiv.className = `chat-message chat-${role}`;
|
| 104 |
-
msgDiv.id = msgId;
|
| 105 |
-
|
| 106 |
-
if (isLoading) {
|
| 107 |
-
msgDiv.classList.add("loading");
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
// Format content with line breaks
|
| 111 |
-
const formatted = content.replace(/\n/g, "<br>");
|
| 112 |
-
|
| 113 |
-
const icon = role === "user" ? "YOU" : role === "assistant" ? "TAC" : "SYS";
|
| 114 |
-
msgDiv.innerHTML = `<span class="chat-icon">${icon}</span><span class="chat-content">${formatted}</span>`;
|
| 115 |
-
|
| 116 |
-
chatMessages.appendChild(msgDiv);
|
| 117 |
-
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 118 |
-
|
| 119 |
-
return msgId;
|
| 120 |
-
}
|
| 121 |
-
|
| 122 |
-
/**
|
| 123 |
-
* Remove a message by ID.
|
| 124 |
-
*/
|
| 125 |
-
function removeMessage(msgId) {
|
| 126 |
-
const msg = document.getElementById(msgId);
|
| 127 |
-
if (msg) msg.remove();
|
| 128 |
-
}
|
| 129 |
-
|
| 130 |
-
/**
|
| 131 |
-
* Clear chat history.
|
| 132 |
-
*/
|
| 133 |
-
function clearChat() {
|
| 134 |
-
const chatMessages = $("#chatMessages");
|
| 135 |
-
if (chatMessages) {
|
| 136 |
-
chatMessages.innerHTML = "";
|
| 137 |
-
}
|
| 138 |
-
chatHistory = [];
|
| 139 |
-
}
|
| 140 |
-
|
| 141 |
-
// Export
|
| 142 |
-
APP.ui = APP.ui || {};
|
| 143 |
-
APP.ui.chat = {
|
| 144 |
-
init,
|
| 145 |
-
sendMessage,
|
| 146 |
-
clearChat
|
| 147 |
-
};
|
| 148 |
-
|
| 149 |
-
// Auto-init on DOMContentLoaded
|
| 150 |
-
if (document.readyState === "loading") {
|
| 151 |
-
document.addEventListener("DOMContentLoaded", init);
|
| 152 |
-
} else {
|
| 153 |
-
init();
|
| 154 |
-
}
|
| 155 |
-
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/cursor.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
| 1 |
-
// Agent Cursor Animation Module
|
| 2 |
-
APP.ui.cursor = {};
|
| 3 |
-
|
| 4 |
-
APP.ui.cursor.ensureAgentCursorOverlay = function () {
|
| 5 |
-
const { $ } = APP.core.utils;
|
| 6 |
-
if ($("#agentCursor")) return;
|
| 7 |
-
|
| 8 |
-
const el = document.createElement("div");
|
| 9 |
-
el.id = "agentCursor";
|
| 10 |
-
el.style.cssText = `
|
| 11 |
-
position: fixed;
|
| 12 |
-
width: 12px;
|
| 13 |
-
height: 12px;
|
| 14 |
-
border-radius: 50%;
|
| 15 |
-
background: linear-gradient(135deg, rgba(34, 211, 238, 0.9), rgba(124, 58, 237, 0.9));
|
| 16 |
-
box-shadow: 0 0 20px rgba(34, 211, 238, 0.6), 0 0 40px rgba(124, 58, 237, 0.4);
|
| 17 |
-
pointer-events: none;
|
| 18 |
-
z-index: 10000;
|
| 19 |
-
opacity: 0;
|
| 20 |
-
display: none;
|
| 21 |
-
transition: opacity 0.3s ease;
|
| 22 |
-
`;
|
| 23 |
-
document.body.appendChild(el);
|
| 24 |
-
};
|
| 25 |
-
|
| 26 |
-
APP.ui.cursor.setCursorVisible = function (visible) {
|
| 27 |
-
const { $ } = APP.core.utils;
|
| 28 |
-
const { state } = APP.core;
|
| 29 |
-
|
| 30 |
-
APP.ui.cursor.ensureAgentCursorOverlay();
|
| 31 |
-
const el = $("#agentCursor");
|
| 32 |
-
|
| 33 |
-
if (!el) return;
|
| 34 |
-
|
| 35 |
-
state.ui.agentCursor.visible = visible;
|
| 36 |
-
el.style.opacity = visible ? "1" : "0";
|
| 37 |
-
el.style.display = visible ? "block" : "none";
|
| 38 |
-
};
|
| 39 |
-
|
| 40 |
-
APP.ui.cursor.moveCursorToRect = function (rect) {
|
| 41 |
-
const { state } = APP.core;
|
| 42 |
-
const { $, now } = APP.core.utils;
|
| 43 |
-
|
| 44 |
-
if (state.ui.cursorMode === "off") return;
|
| 45 |
-
|
| 46 |
-
APP.ui.cursor.ensureAgentCursorOverlay();
|
| 47 |
-
const el = $("#agentCursor");
|
| 48 |
-
|
| 49 |
-
if (!el) return;
|
| 50 |
-
|
| 51 |
-
const c = state.ui.agentCursor;
|
| 52 |
-
c.visible = true;
|
| 53 |
-
c.target = rect;
|
| 54 |
-
c.t0 = now();
|
| 55 |
-
el.style.opacity = "1";
|
| 56 |
-
el.style.display = "block";
|
| 57 |
-
};
|
| 58 |
-
|
| 59 |
-
APP.ui.cursor.tickAgentCursor = function () {
|
| 60 |
-
const { state } = APP.core;
|
| 61 |
-
const { $, clamp, now } = APP.core.utils;
|
| 62 |
-
const el = $("#agentCursor");
|
| 63 |
-
|
| 64 |
-
if (!el || state.ui.cursorMode !== "on" || !state.ui.agentCursor.visible) return;
|
| 65 |
-
|
| 66 |
-
const c = state.ui.agentCursor;
|
| 67 |
-
if (!c.target) return;
|
| 68 |
-
|
| 69 |
-
const tx = c.target.left + c.target.width * 0.72;
|
| 70 |
-
const ty = c.target.top + c.target.height * 0.50;
|
| 71 |
-
|
| 72 |
-
// Smooth spring physics
|
| 73 |
-
const dx = tx - (c.x * window.innerWidth);
|
| 74 |
-
const dy = ty - (c.y * window.innerHeight);
|
| 75 |
-
c.vx = (c.vx + dx * 0.0018) * 0.85;
|
| 76 |
-
c.vy = (c.vy + dy * 0.0018) * 0.85;
|
| 77 |
-
|
| 78 |
-
const px = (c.x * window.innerWidth) + c.vx * 18;
|
| 79 |
-
const py = (c.y * window.innerHeight) + c.vy * 18;
|
| 80 |
-
c.x = clamp(px / window.innerWidth, 0.02, 0.98);
|
| 81 |
-
c.y = clamp(py / window.innerHeight, 0.02, 0.98);
|
| 82 |
-
|
| 83 |
-
el.style.transform = `translate(${c.x * window.innerWidth}px, ${c.y * window.innerHeight}px)`;
|
| 84 |
-
|
| 85 |
-
// Hide after settling
|
| 86 |
-
const settle = Math.hypot(dx, dy);
|
| 87 |
-
if (settle < 6 && (now() - c.t0) > 650) {
|
| 88 |
-
el.style.opacity = "0.75";
|
| 89 |
-
}
|
| 90 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/logging.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
| 1 |
-
APP.ui.logging = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.logging.log = function (msg, type = "i") {
|
| 4 |
-
const { $ } = APP.core.utils;
|
| 5 |
-
const consoleHook = $("#sysLog"); // Fixed ID: sysLog from html
|
| 6 |
-
if (!consoleHook) return;
|
| 7 |
-
const div = document.createElement("div");
|
| 8 |
-
div.className = "log-line";
|
| 9 |
-
const ts = new Date().toISOString().split("T")[1].slice(0, 8);
|
| 10 |
-
let color = "#94a3b8"; // dim
|
| 11 |
-
if (type === "g") color = "var(--good)";
|
| 12 |
-
if (type === "w") color = "var(--warn)";
|
| 13 |
-
if (type === "e") color = "var(--bad)";
|
| 14 |
-
if (type === "t") color = "var(--theme)";
|
| 15 |
-
|
| 16 |
-
div.innerHTML = `<span class="ts">[${ts}]</span> <span style="color:${color}">${msg}</span>`;
|
| 17 |
-
consoleHook.appendChild(div);
|
| 18 |
-
consoleHook.scrollTop = consoleHook.scrollHeight;
|
| 19 |
-
};
|
| 20 |
-
|
| 21 |
-
APP.ui.logging.setStatus = function (level, text) {
|
| 22 |
-
const { $ } = APP.core.utils;
|
| 23 |
-
const sysDot = $("#sys-dot");
|
| 24 |
-
const sysStatus = $("#sys-status");
|
| 25 |
-
if (!sysDot || !sysStatus) return;
|
| 26 |
-
let color = "var(--text-dim)";
|
| 27 |
-
if (level === "good") color = "var(--good)";
|
| 28 |
-
if (level === "warn") color = "var(--warn)";
|
| 29 |
-
if (level === "bad") color = "var(--bad)";
|
| 30 |
-
|
| 31 |
-
sysDot.style.background = color;
|
| 32 |
-
sysDot.style.boxShadow = `0 0 10px ${color}`;
|
| 33 |
-
sysStatus.textContent = text;
|
| 34 |
-
sysStatus.style.color = color === "var(--text-dim)" ? "var(--text-main)" : color;
|
| 35 |
-
};
|
| 36 |
-
|
| 37 |
-
APP.ui.logging.setHfStatus = function (msg) {
|
| 38 |
-
const { $ } = APP.core.utils;
|
| 39 |
-
const el = $("#hfBackendStatus");
|
| 40 |
-
if (el) el.textContent = `HF Backend: ${msg}`;
|
| 41 |
-
|
| 42 |
-
if (msg && (msg.toLowerCase().includes("error") || msg.toLowerCase().includes("failed"))) {
|
| 43 |
-
APP.ui.logging.log(msg, "e");
|
| 44 |
-
}
|
| 45 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/js/ui/overlays.js
DELETED
|
@@ -1,113 +0,0 @@
|
|
| 1 |
-
APP.ui.overlays = {};
|
| 2 |
-
|
| 3 |
-
APP.ui.overlays.render = function (canvasId, trackSource) {
|
| 4 |
-
const { state } = APP.core;
|
| 5 |
-
const { $ } = APP.core.utils;
|
| 6 |
-
|
| 7 |
-
const canvas = $(`#${canvasId}`);
|
| 8 |
-
if (!canvas) return;
|
| 9 |
-
|
| 10 |
-
const ctx = canvas.getContext("2d");
|
| 11 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 12 |
-
|
| 13 |
-
const tracks = trackSource || [];
|
| 14 |
-
if (!tracks.length) return;
|
| 15 |
-
|
| 16 |
-
const w = canvas.width;
|
| 17 |
-
const h = canvas.height;
|
| 18 |
-
const selId = state.selectedId || state.tracker.selectedTrackId;
|
| 19 |
-
|
| 20 |
-
// Draw selected track highlight (always, even on processed feed)
|
| 21 |
-
if (selId) {
|
| 22 |
-
const sel = tracks.find(t => t.id === selId);
|
| 23 |
-
if (sel && sel.bbox) {
|
| 24 |
-
const b = sel.bbox;
|
| 25 |
-
const px = b.x * w, py = b.y * h, pw = b.w * w, ph = b.h * h;
|
| 26 |
-
|
| 27 |
-
// Subtle filled highlight
|
| 28 |
-
ctx.fillStyle = "rgba(124, 58, 237, 0.15)";
|
| 29 |
-
ctx.fillRect(px, py, pw, ph);
|
| 30 |
-
|
| 31 |
-
// Border glow (double stroke for glow effect)
|
| 32 |
-
ctx.strokeStyle = "rgba(124, 58, 237, 0.6)";
|
| 33 |
-
ctx.lineWidth = 3;
|
| 34 |
-
ctx.strokeRect(px, py, pw, ph);
|
| 35 |
-
ctx.strokeStyle = "rgba(34, 211, 238, 0.4)";
|
| 36 |
-
ctx.lineWidth = 1;
|
| 37 |
-
ctx.strokeRect(px - 1, py - 1, pw + 2, ph + 2);
|
| 38 |
-
|
| 39 |
-
// Small label tag
|
| 40 |
-
const label = `${sel.id}`;
|
| 41 |
-
ctx.font = "bold 11px monospace";
|
| 42 |
-
const tw = ctx.measureText(label).width + 8;
|
| 43 |
-
ctx.fillStyle = "rgba(124, 58, 237, 0.8)";
|
| 44 |
-
ctx.fillRect(px, py - 18, tw, 16);
|
| 45 |
-
ctx.fillStyle = "#fff";
|
| 46 |
-
ctx.fillText(label, px + 4, py - 5);
|
| 47 |
-
}
|
| 48 |
-
}
|
| 49 |
-
};
|
| 50 |
-
|
| 51 |
-
APP.ui.overlays.renderFrameOverlay = function () {
|
| 52 |
-
const { state } = APP.core;
|
| 53 |
-
const { $ } = APP.core.utils;
|
| 54 |
-
const canvas = $("#frameOverlay");
|
| 55 |
-
if (!canvas) return;
|
| 56 |
-
|
| 57 |
-
if (state.selectedId) {
|
| 58 |
-
const sel = (state.detections || []).filter(d => d.id === state.selectedId);
|
| 59 |
-
if (sel.length) {
|
| 60 |
-
APP.ui.overlays.render("frameOverlay", sel);
|
| 61 |
-
return;
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
const ctx = canvas.getContext("2d");
|
| 65 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 66 |
-
};
|
| 67 |
-
|
| 68 |
-
APP.ui.overlays.initClickHandler = function () {
|
| 69 |
-
const { $ } = APP.core.utils;
|
| 70 |
-
const canvas = $("#engageOverlay");
|
| 71 |
-
if (!canvas) return;
|
| 72 |
-
|
| 73 |
-
canvas.style.pointerEvents = "auto";
|
| 74 |
-
canvas.style.cursor = "crosshair";
|
| 75 |
-
|
| 76 |
-
canvas.addEventListener("click", (e) => {
|
| 77 |
-
const { state } = APP.core;
|
| 78 |
-
const tracks = state.tracker.tracks;
|
| 79 |
-
if (!tracks || !tracks.length) return;
|
| 80 |
-
|
| 81 |
-
// Convert click to normalized 0-1 coords relative to canvas
|
| 82 |
-
const rect = canvas.getBoundingClientRect();
|
| 83 |
-
const nx = (e.clientX - rect.left) / rect.width;
|
| 84 |
-
const ny = (e.clientY - rect.top) / rect.height;
|
| 85 |
-
|
| 86 |
-
// Hit-test against track bboxes (smallest area wins for overlaps)
|
| 87 |
-
// Margin accounts for drift between visual bbox and tracked position
|
| 88 |
-
const margin = 0.015;
|
| 89 |
-
let best = null;
|
| 90 |
-
let bestArea = Infinity;
|
| 91 |
-
|
| 92 |
-
for (const t of tracks) {
|
| 93 |
-
const b = t.bbox;
|
| 94 |
-
if (!b) continue;
|
| 95 |
-
if (nx >= b.x - margin && nx <= b.x + b.w + margin &&
|
| 96 |
-
ny >= b.y - margin && ny <= b.y + b.h + margin) {
|
| 97 |
-
const area = b.w * b.h;
|
| 98 |
-
if (area < bestArea) {
|
| 99 |
-
bestArea = area;
|
| 100 |
-
best = t;
|
| 101 |
-
}
|
| 102 |
-
}
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
const id = best ? best.id : null;
|
| 106 |
-
document.dispatchEvent(new CustomEvent("track-selected", { detail: { id } }));
|
| 107 |
-
});
|
| 108 |
-
};
|
| 109 |
-
|
| 110 |
-
APP.ui.overlays.renderEngageOverlay = function () {
|
| 111 |
-
const { state } = APP.core;
|
| 112 |
-
APP.ui.overlays.render("engageOverlay", state.tracker.tracks);
|
| 113 |
-
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/style.css
DELETED
|
@@ -1,1142 +0,0 @@
|
|
| 1 |
-
/* =========================================
|
| 2 |
-
LaserPerception Design System
|
| 3 |
-
========================================= */
|
| 4 |
-
|
| 5 |
-
:root {
|
| 6 |
-
/* --- Colors --- */
|
| 7 |
-
--bg: #060914;
|
| 8 |
-
--panel: #0b1026;
|
| 9 |
-
--panel2: #0a0f22;
|
| 10 |
-
|
| 11 |
-
--stroke: rgba(255, 255, 255, .08);
|
| 12 |
-
--stroke2: rgba(255, 255, 255, .12);
|
| 13 |
-
|
| 14 |
-
--text: rgba(255, 255, 255, .92);
|
| 15 |
-
--muted: rgba(255, 255, 255, .62);
|
| 16 |
-
--faint: rgba(255, 255, 255, .42);
|
| 17 |
-
|
| 18 |
-
--good: #22c55e;
|
| 19 |
-
--warn: #f59e0b;
|
| 20 |
-
--bad: #ef4444;
|
| 21 |
-
|
| 22 |
-
--accent: #7c3aed;
|
| 23 |
-
--cyan: #22d3ee;
|
| 24 |
-
--mag: #fb7185;
|
| 25 |
-
|
| 26 |
-
/* --- Effects --- */
|
| 27 |
-
--shadow: 0 18px 60px rgba(0, 0, 0, .55);
|
| 28 |
-
|
| 29 |
-
/* --- Typography --- */
|
| 30 |
-
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 31 |
-
--sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
* {
|
| 35 |
-
box-sizing: border-box;
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
html,
|
| 39 |
-
body {
|
| 40 |
-
height: 100%;
|
| 41 |
-
margin: 0;
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
body {
|
| 45 |
-
background:
|
| 46 |
-
radial-gradient(1200px 700px at 20% 8%, rgba(124, 58, 237, .22), transparent 60%),
|
| 47 |
-
radial-gradient(900px 500px at 82% 18%, rgba(34, 211, 238, .18), transparent 60%),
|
| 48 |
-
radial-gradient(800px 520px at 52% 82%, rgba(251, 113, 133, .10), transparent 65%),
|
| 49 |
-
linear-gradient(180deg, #040614, #060914);
|
| 50 |
-
color: var(--text);
|
| 51 |
-
font-family: var(--sans);
|
| 52 |
-
overflow: hidden;
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
/* =========================================
|
| 56 |
-
Layout & Structure
|
| 57 |
-
========================================= */
|
| 58 |
-
|
| 59 |
-
#app {
|
| 60 |
-
height: 100%;
|
| 61 |
-
display: flex;
|
| 62 |
-
flex-direction: column;
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
header {
|
| 66 |
-
display: flex;
|
| 67 |
-
align-items: center;
|
| 68 |
-
justify-content: space-between;
|
| 69 |
-
padding: 14px 16px 12px;
|
| 70 |
-
border-bottom: 1px solid var(--stroke);
|
| 71 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, .035), transparent);
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
.workspace {
|
| 75 |
-
flex: 1;
|
| 76 |
-
display: grid;
|
| 77 |
-
grid-template-columns: 270px 1fr;
|
| 78 |
-
/* Fixed sidebar width - reduced by 50% */
|
| 79 |
-
gap: 12px;
|
| 80 |
-
padding: 12px;
|
| 81 |
-
min-height: 0;
|
| 82 |
-
}
|
| 83 |
-
|
| 84 |
-
aside {
|
| 85 |
-
background: rgba(255, 255, 255, .02);
|
| 86 |
-
border: 1px solid var(--stroke);
|
| 87 |
-
border-radius: 16px;
|
| 88 |
-
box-shadow: var(--shadow);
|
| 89 |
-
overflow-y: auto;
|
| 90 |
-
overflow-x: hidden;
|
| 91 |
-
display: flex;
|
| 92 |
-
flex-direction: column;
|
| 93 |
-
min-height: 0;
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
main {
|
| 97 |
-
background: rgba(255, 255, 255, .02);
|
| 98 |
-
border: 1px solid var(--stroke);
|
| 99 |
-
border-radius: 16px;
|
| 100 |
-
box-shadow: var(--shadow);
|
| 101 |
-
overflow: hidden;
|
| 102 |
-
display: flex;
|
| 103 |
-
flex-direction: column;
|
| 104 |
-
min-height: 0;
|
| 105 |
-
}
|
| 106 |
-
|
| 107 |
-
footer {
|
| 108 |
-
padding: 10px 14px;
|
| 109 |
-
border-top: 1px solid var(--stroke);
|
| 110 |
-
color: var(--muted);
|
| 111 |
-
font-size: 11px;
|
| 112 |
-
display: flex;
|
| 113 |
-
justify-content: space-between;
|
| 114 |
-
align-items: center;
|
| 115 |
-
gap: 10px;
|
| 116 |
-
background: linear-gradient(0deg, rgba(255, 255, 255, .03), transparent);
|
| 117 |
-
}
|
| 118 |
-
|
| 119 |
-
footer .mono {
|
| 120 |
-
font-family: var(--mono);
|
| 121 |
-
color: rgba(255, 255, 255, .76);
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
/* =========================================
|
| 125 |
-
Brand & Status
|
| 126 |
-
========================================= */
|
| 127 |
-
|
| 128 |
-
.brand {
|
| 129 |
-
display: flex;
|
| 130 |
-
gap: 12px;
|
| 131 |
-
align-items: center;
|
| 132 |
-
min-width: 420px;
|
| 133 |
-
}
|
| 134 |
-
|
| 135 |
-
.logo {
|
| 136 |
-
width: 40px;
|
| 137 |
-
height: 40px;
|
| 138 |
-
border-radius: 14px;
|
| 139 |
-
background:
|
| 140 |
-
radial-gradient(circle at 30% 30%, rgba(34, 211, 238, .9), rgba(124, 58, 237, .9) 55%, rgba(0, 0, 0, .1) 70%),
|
| 141 |
-
linear-gradient(135deg, rgba(255, 255, 255, .10), transparent 60%);
|
| 142 |
-
box-shadow: 0 16px 46px rgba(124, 58, 237, .25);
|
| 143 |
-
border: 1px solid rgba(255, 255, 255, .16);
|
| 144 |
-
position: relative;
|
| 145 |
-
overflow: hidden;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
-
.logo:after {
|
| 149 |
-
content: "";
|
| 150 |
-
position: absolute;
|
| 151 |
-
inset: -40px;
|
| 152 |
-
background: conic-gradient(from 180deg, transparent, rgba(255, 255, 255, .10), transparent);
|
| 153 |
-
animation: spin 10s linear infinite;
|
| 154 |
-
}
|
| 155 |
-
|
| 156 |
-
@keyframes spin {
|
| 157 |
-
to {
|
| 158 |
-
transform: rotate(360deg);
|
| 159 |
-
}
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
.brand h1 {
|
| 163 |
-
font-size: 14px;
|
| 164 |
-
margin: 0;
|
| 165 |
-
letter-spacing: .16em;
|
| 166 |
-
text-transform: uppercase;
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
.brand .sub {
|
| 170 |
-
font-size: 12px;
|
| 171 |
-
color: var(--muted);
|
| 172 |
-
margin-top: 2px;
|
| 173 |
-
line-height: 1.2;
|
| 174 |
-
}
|
| 175 |
-
|
| 176 |
-
.status-row {
|
| 177 |
-
display: flex;
|
| 178 |
-
gap: 10px;
|
| 179 |
-
align-items: center;
|
| 180 |
-
flex-wrap: wrap;
|
| 181 |
-
justify-content: flex-end;
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
/* =========================================
|
| 185 |
-
Components: Cards & Panels
|
| 186 |
-
========================================= */
|
| 187 |
-
|
| 188 |
-
.card {
|
| 189 |
-
padding: 12px 12px 10px;
|
| 190 |
-
border-bottom: 1px solid var(--stroke);
|
| 191 |
-
position: relative;
|
| 192 |
-
}
|
| 193 |
-
|
| 194 |
-
.card:last-child {
|
| 195 |
-
border-bottom: none;
|
| 196 |
-
}
|
| 197 |
-
|
| 198 |
-
.card h2 {
|
| 199 |
-
margin: 0;
|
| 200 |
-
font-size: 12px;
|
| 201 |
-
letter-spacing: .14em;
|
| 202 |
-
text-transform: uppercase;
|
| 203 |
-
color: rgba(255, 255, 255, .78);
|
| 204 |
-
}
|
| 205 |
-
|
| 206 |
-
.card small {
|
| 207 |
-
color: var(--muted);
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
.card .hint {
|
| 211 |
-
color: var(--faint);
|
| 212 |
-
font-size: 11px;
|
| 213 |
-
line-height: 1.35;
|
| 214 |
-
margin-top: 6px;
|
| 215 |
-
}
|
| 216 |
-
|
| 217 |
-
.panel {
|
| 218 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, .03), rgba(255, 255, 255, .015));
|
| 219 |
-
border: 1px solid var(--stroke);
|
| 220 |
-
border-radius: 16px;
|
| 221 |
-
padding: 10px;
|
| 222 |
-
box-shadow: 0 10px 30px rgba(0, 0, 0, .35);
|
| 223 |
-
overflow: hidden;
|
| 224 |
-
position: relative;
|
| 225 |
-
}
|
| 226 |
-
|
| 227 |
-
.panel h3 {
|
| 228 |
-
margin: 0 0 8px;
|
| 229 |
-
font-size: 12px;
|
| 230 |
-
letter-spacing: .14em;
|
| 231 |
-
text-transform: uppercase;
|
| 232 |
-
color: rgba(255, 255, 255, .78);
|
| 233 |
-
display: flex;
|
| 234 |
-
align-items: center;
|
| 235 |
-
justify-content: space-between;
|
| 236 |
-
gap: 8px;
|
| 237 |
-
}
|
| 238 |
-
|
| 239 |
-
.panel h3 .rightnote {
|
| 240 |
-
font-size: 11px;
|
| 241 |
-
color: var(--muted);
|
| 242 |
-
font-family: var(--mono);
|
| 243 |
-
letter-spacing: 0;
|
| 244 |
-
text-transform: none;
|
| 245 |
-
}
|
| 246 |
-
|
| 247 |
-
.collapse-btn {
|
| 248 |
-
background: rgba(255, 255, 255, .05);
|
| 249 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 250 |
-
border-radius: 8px;
|
| 251 |
-
padding: 4px 8px;
|
| 252 |
-
color: var(--muted);
|
| 253 |
-
cursor: pointer;
|
| 254 |
-
font-size: 11px;
|
| 255 |
-
font-family: var(--mono);
|
| 256 |
-
transition: all 0.2s ease;
|
| 257 |
-
text-transform: none;
|
| 258 |
-
letter-spacing: 0;
|
| 259 |
-
}
|
| 260 |
-
|
| 261 |
-
.collapse-btn:hover {
|
| 262 |
-
background: rgba(255, 255, 255, .08);
|
| 263 |
-
color: var(--text);
|
| 264 |
-
border-color: rgba(255, 255, 255, .18);
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
/* =========================================
|
| 268 |
-
Components: Inputs & Controls
|
| 269 |
-
========================================= */
|
| 270 |
-
|
| 271 |
-
.grid2 {
|
| 272 |
-
display: grid;
|
| 273 |
-
grid-template-columns: 1fr 1fr;
|
| 274 |
-
gap: 8px;
|
| 275 |
-
margin-top: 10px;
|
| 276 |
-
}
|
| 277 |
-
|
| 278 |
-
.row {
|
| 279 |
-
display: flex;
|
| 280 |
-
gap: 8px;
|
| 281 |
-
align-items: center;
|
| 282 |
-
justify-content: space-between;
|
| 283 |
-
margin-top: 8px;
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
label {
|
| 287 |
-
font-size: 11px;
|
| 288 |
-
color: var(--muted);
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
input[type="range"] {
|
| 292 |
-
width: 100%;
|
| 293 |
-
}
|
| 294 |
-
|
| 295 |
-
select,
|
| 296 |
-
textarea,
|
| 297 |
-
input[type="text"],
|
| 298 |
-
input[type="number"] {
|
| 299 |
-
width: 100%;
|
| 300 |
-
background: rgba(255, 255, 255, .04);
|
| 301 |
-
border: 1px solid var(--stroke2);
|
| 302 |
-
border-radius: 10px;
|
| 303 |
-
padding: 8px 10px;
|
| 304 |
-
color: var(--text);
|
| 305 |
-
outline: none;
|
| 306 |
-
font-size: 12px;
|
| 307 |
-
}
|
| 308 |
-
|
| 309 |
-
select:focus,
|
| 310 |
-
textarea:focus,
|
| 311 |
-
input[type="text"]:focus,
|
| 312 |
-
input[type="number"]:focus {
|
| 313 |
-
border-color: rgba(124, 58, 237, .55);
|
| 314 |
-
box-shadow: 0 0 0 3px rgba(124, 58, 237, .16);
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
.btn {
|
| 318 |
-
user-select: none;
|
| 319 |
-
cursor: pointer;
|
| 320 |
-
border: none;
|
| 321 |
-
border-radius: 12px;
|
| 322 |
-
padding: 10px 12px;
|
| 323 |
-
font-weight: 700;
|
| 324 |
-
font-size: 12px;
|
| 325 |
-
letter-spacing: .04em;
|
| 326 |
-
color: rgba(255, 255, 255, .92);
|
| 327 |
-
background: linear-gradient(135deg, rgba(124, 58, 237, .95), rgba(34, 211, 238, .45));
|
| 328 |
-
box-shadow: 0 18px 40px rgba(124, 58, 237, .24);
|
| 329 |
-
}
|
| 330 |
-
|
| 331 |
-
.btn:hover {
|
| 332 |
-
filter: brightness(1.06);
|
| 333 |
-
}
|
| 334 |
-
|
| 335 |
-
.btn:active {
|
| 336 |
-
transform: translateY(1px);
|
| 337 |
-
}
|
| 338 |
-
|
| 339 |
-
.btn.secondary {
|
| 340 |
-
background: rgba(255, 255, 255, .06);
|
| 341 |
-
border: 1px solid var(--stroke2);
|
| 342 |
-
box-shadow: none;
|
| 343 |
-
font-weight: 600;
|
| 344 |
-
}
|
| 345 |
-
|
| 346 |
-
.btn.secondary:hover {
|
| 347 |
-
background: rgba(255, 255, 255, .08);
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
.btn.danger {
|
| 351 |
-
background: linear-gradient(135deg, rgba(239, 68, 68, .95), rgba(251, 113, 133, .55));
|
| 352 |
-
box-shadow: 0 18px 40px rgba(239, 68, 68, .18);
|
| 353 |
-
}
|
| 354 |
-
|
| 355 |
-
.btnrow {
|
| 356 |
-
display: flex;
|
| 357 |
-
gap: 8px;
|
| 358 |
-
margin-top: 10px;
|
| 359 |
-
}
|
| 360 |
-
|
| 361 |
-
.btnrow .btn {
|
| 362 |
-
flex: 1;
|
| 363 |
-
}
|
| 364 |
-
|
| 365 |
-
.pill {
|
| 366 |
-
display: flex;
|
| 367 |
-
align-items: center;
|
| 368 |
-
gap: 10px;
|
| 369 |
-
padding: 8px 12px;
|
| 370 |
-
border-radius: 999px;
|
| 371 |
-
border: 1px solid var(--stroke2);
|
| 372 |
-
background: rgba(255, 255, 255, .04);
|
| 373 |
-
box-shadow: 0 10px 26px rgba(0, 0, 0, .35);
|
| 374 |
-
font-size: 12px;
|
| 375 |
-
color: var(--muted);
|
| 376 |
-
white-space: nowrap;
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
.dot {
|
| 380 |
-
width: 8px;
|
| 381 |
-
height: 8px;
|
| 382 |
-
border-radius: 50%;
|
| 383 |
-
background: var(--good);
|
| 384 |
-
box-shadow: 0 0 16px rgba(34, 197, 94, .6);
|
| 385 |
-
}
|
| 386 |
-
|
| 387 |
-
.dot.warn {
|
| 388 |
-
background: var(--warn);
|
| 389 |
-
box-shadow: 0 0 16px rgba(245, 158, 11, .55);
|
| 390 |
-
}
|
| 391 |
-
|
| 392 |
-
.dot.bad {
|
| 393 |
-
background: var(--bad);
|
| 394 |
-
box-shadow: 0 0 16px rgba(239, 68, 68, .55);
|
| 395 |
-
}
|
| 396 |
-
|
| 397 |
-
.kbd {
|
| 398 |
-
font-family: var(--mono);
|
| 399 |
-
font-size: 11px;
|
| 400 |
-
padding: 2px 6px;
|
| 401 |
-
border: 1px solid var(--stroke2);
|
| 402 |
-
border-bottom-color: rgba(255, 255, 255, .24);
|
| 403 |
-
background: rgba(0, 0, 0, .35);
|
| 404 |
-
border-radius: 7px;
|
| 405 |
-
color: rgba(255, 255, 255, .78);
|
| 406 |
-
}
|
| 407 |
-
|
| 408 |
-
.badge {
|
| 409 |
-
display: inline-flex;
|
| 410 |
-
align-items: center;
|
| 411 |
-
gap: 6px;
|
| 412 |
-
padding: 4px 8px;
|
| 413 |
-
border-radius: 999px;
|
| 414 |
-
border: 1px solid var(--stroke2);
|
| 415 |
-
background: rgba(0, 0, 0, .25);
|
| 416 |
-
font-family: var(--mono);
|
| 417 |
-
}
|
| 418 |
-
|
| 419 |
-
/* =========================================
|
| 420 |
-
Navigation: Tabs
|
| 421 |
-
========================================= */
|
| 422 |
-
|
| 423 |
-
.tabs {
|
| 424 |
-
display: flex;
|
| 425 |
-
gap: 8px;
|
| 426 |
-
padding: 10px 12px;
|
| 427 |
-
border-bottom: 1px solid var(--stroke);
|
| 428 |
-
background: linear-gradient(180deg, rgba(255, 255, 255, .035), transparent);
|
| 429 |
-
flex-wrap: wrap;
|
| 430 |
-
}
|
| 431 |
-
|
| 432 |
-
.tabbtn {
|
| 433 |
-
cursor: pointer;
|
| 434 |
-
border: none;
|
| 435 |
-
border-radius: 999px;
|
| 436 |
-
padding: 8px 12px;
|
| 437 |
-
font-size: 12px;
|
| 438 |
-
color: rgba(255, 255, 255, .75);
|
| 439 |
-
background: rgba(255, 255, 255, .04);
|
| 440 |
-
border: 1px solid var(--stroke2);
|
| 441 |
-
}
|
| 442 |
-
|
| 443 |
-
.tabbtn.active {
|
| 444 |
-
color: rgba(255, 255, 255, .92);
|
| 445 |
-
background: linear-gradient(135deg, rgba(124, 58, 237, .35), rgba(34, 211, 238, .10));
|
| 446 |
-
border-color: rgba(124, 58, 237, .45);
|
| 447 |
-
box-shadow: 0 0 0 3px rgba(124, 58, 237, .14);
|
| 448 |
-
}
|
| 449 |
-
|
| 450 |
-
.tab {
|
| 451 |
-
display: none;
|
| 452 |
-
flex: 1;
|
| 453 |
-
min-height: 0;
|
| 454 |
-
overflow: auto;
|
| 455 |
-
padding: 12px;
|
| 456 |
-
}
|
| 457 |
-
|
| 458 |
-
.tab.active {
|
| 459 |
-
display: block;
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
/* =========================================
|
| 463 |
-
Visualization: Views & Canvas
|
| 464 |
-
========================================= */
|
| 465 |
-
|
| 466 |
-
.viewbox {
|
| 467 |
-
position: relative;
|
| 468 |
-
border-radius: 14px;
|
| 469 |
-
overflow: hidden;
|
| 470 |
-
background: radial-gradient(700px 380px at 30% 30%, rgba(124, 58, 237, .12), rgba(0, 0, 0, .0) 60%),
|
| 471 |
-
linear-gradient(180deg, rgba(0, 0, 0, .25), rgba(0, 0, 0, .15));
|
| 472 |
-
border: 1px solid rgba(255, 255, 255, .08);
|
| 473 |
-
min-height: 360px;
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
.viewbox canvas,
|
| 477 |
-
.viewbox video {
|
| 478 |
-
width: 100%;
|
| 479 |
-
height: 100%;
|
| 480 |
-
display: block;
|
| 481 |
-
object-fit: contain;
|
| 482 |
-
/* Maintain aspect ratio, letterbox if needed */
|
| 483 |
-
}
|
| 484 |
-
|
| 485 |
-
/* Always show the engage video feed */
|
| 486 |
-
#videoEngage {
|
| 487 |
-
display: block;
|
| 488 |
-
opacity: 1;
|
| 489 |
-
}
|
| 490 |
-
|
| 491 |
-
/* Overlay must use same object-fit as the main canvas to align properly */
|
| 492 |
-
.viewbox .overlay {
|
| 493 |
-
position: absolute;
|
| 494 |
-
top: 0;
|
| 495 |
-
left: 0;
|
| 496 |
-
width: 100%;
|
| 497 |
-
height: 100%;
|
| 498 |
-
pointer-events: none;
|
| 499 |
-
object-fit: contain;
|
| 500 |
-
/* CRITICAL: match frameCanvas object-fit */
|
| 501 |
-
}
|
| 502 |
-
|
| 503 |
-
/* Engage overlay: transparent by default, receives clicks for track selection */
|
| 504 |
-
#engageOverlay {
|
| 505 |
-
pointer-events: auto;
|
| 506 |
-
cursor: crosshair;
|
| 507 |
-
}
|
| 508 |
-
|
| 509 |
-
.viewbox .watermark {
|
| 510 |
-
position: absolute;
|
| 511 |
-
left: 10px;
|
| 512 |
-
bottom: 10px;
|
| 513 |
-
font-family: var(--mono);
|
| 514 |
-
font-size: 11px;
|
| 515 |
-
color: rgba(255, 255, 255, .55);
|
| 516 |
-
background: rgba(0, 0, 0, .35);
|
| 517 |
-
border: 1px solid rgba(255, 255, 255, .14);
|
| 518 |
-
padding: 6px 8px;
|
| 519 |
-
border-radius: 10px;
|
| 520 |
-
}
|
| 521 |
-
|
| 522 |
-
.viewbox .empty {
|
| 523 |
-
position: absolute;
|
| 524 |
-
inset: 0;
|
| 525 |
-
display: flex;
|
| 526 |
-
flex-direction: column;
|
| 527 |
-
align-items: center;
|
| 528 |
-
justify-content: center;
|
| 529 |
-
gap: 10px;
|
| 530 |
-
color: rgba(255, 255, 255, .72);
|
| 531 |
-
text-align: center;
|
| 532 |
-
padding: 22px;
|
| 533 |
-
}
|
| 534 |
-
|
| 535 |
-
.viewbox .empty .big {
|
| 536 |
-
font-size: 14px;
|
| 537 |
-
letter-spacing: .12em;
|
| 538 |
-
text-transform: uppercase;
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
.viewbox .empty .small {
|
| 542 |
-
color: var(--muted);
|
| 543 |
-
font-size: 12px;
|
| 544 |
-
max-width: 520px;
|
| 545 |
-
line-height: 1.4;
|
| 546 |
-
}
|
| 547 |
-
|
| 548 |
-
/* =========================================
|
| 549 |
-
Lists & Tables
|
| 550 |
-
========================================= */
|
| 551 |
-
|
| 552 |
-
.list {
|
| 553 |
-
display: flex;
|
| 554 |
-
flex-direction: column;
|
| 555 |
-
gap: 6px;
|
| 556 |
-
min-height: 160px;
|
| 557 |
-
max-height: none;
|
| 558 |
-
overflow-y: auto;
|
| 559 |
-
padding-right: 4px;
|
| 560 |
-
}
|
| 561 |
-
|
| 562 |
-
.obj {
|
| 563 |
-
padding: 10px;
|
| 564 |
-
border-radius: 14px;
|
| 565 |
-
border: 1px solid var(--stroke2);
|
| 566 |
-
background: rgba(255, 255, 255, .03);
|
| 567 |
-
cursor: pointer;
|
| 568 |
-
}
|
| 569 |
-
|
| 570 |
-
.obj:hover {
|
| 571 |
-
background: rgba(255, 255, 255, .05);
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
-
.obj.active {
|
| 575 |
-
border-color: rgba(34, 211, 238, .45);
|
| 576 |
-
box-shadow: 0 0 0 3px rgba(34, 211, 238, .14);
|
| 577 |
-
background: linear-gradient(135deg, rgba(34, 211, 238, .10), rgba(124, 58, 237, .08));
|
| 578 |
-
}
|
| 579 |
-
|
| 580 |
-
.obj .top {
|
| 581 |
-
display: flex;
|
| 582 |
-
align-items: center;
|
| 583 |
-
justify-content: space-between;
|
| 584 |
-
gap: 10px;
|
| 585 |
-
}
|
| 586 |
-
|
| 587 |
-
.obj .id {
|
| 588 |
-
font-family: var(--mono);
|
| 589 |
-
font-size: 12px;
|
| 590 |
-
color: rgba(255, 255, 255, .90);
|
| 591 |
-
}
|
| 592 |
-
|
| 593 |
-
.obj .cls {
|
| 594 |
-
font-size: 12px;
|
| 595 |
-
color: rgba(255, 255, 255, .80);
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
.obj .meta {
|
| 599 |
-
margin-top: 6px;
|
| 600 |
-
display: flex;
|
| 601 |
-
gap: 10px;
|
| 602 |
-
flex-wrap: wrap;
|
| 603 |
-
font-size: 11px;
|
| 604 |
-
color: var(--muted);
|
| 605 |
-
}
|
| 606 |
-
|
| 607 |
-
.table {
|
| 608 |
-
width: 100%;
|
| 609 |
-
border-collapse: separate;
|
| 610 |
-
border-spacing: 0;
|
| 611 |
-
overflow: hidden;
|
| 612 |
-
border-radius: 14px;
|
| 613 |
-
border: 1px solid rgba(255, 255, 255, .10);
|
| 614 |
-
}
|
| 615 |
-
|
| 616 |
-
.table th,
|
| 617 |
-
.table td {
|
| 618 |
-
padding: 8px 10px;
|
| 619 |
-
font-size: 12px;
|
| 620 |
-
border-bottom: 1px solid rgba(255, 255, 255, .08);
|
| 621 |
-
vertical-align: top;
|
| 622 |
-
}
|
| 623 |
-
|
| 624 |
-
.table th {
|
| 625 |
-
background: rgba(255, 255, 255, .04);
|
| 626 |
-
color: rgba(255, 255, 255, .78);
|
| 627 |
-
letter-spacing: .12em;
|
| 628 |
-
text-transform: uppercase;
|
| 629 |
-
font-size: 11px;
|
| 630 |
-
}
|
| 631 |
-
|
| 632 |
-
.table tr:last-child td {
|
| 633 |
-
border-bottom: none;
|
| 634 |
-
}
|
| 635 |
-
|
| 636 |
-
.k {
|
| 637 |
-
font-family: var(--mono);
|
| 638 |
-
color: rgba(255, 255, 255, .84);
|
| 639 |
-
}
|
| 640 |
-
|
| 641 |
-
.mini {
|
| 642 |
-
font-size: 11px;
|
| 643 |
-
color: var(--muted);
|
| 644 |
-
line-height: 1.35;
|
| 645 |
-
}
|
| 646 |
-
|
| 647 |
-
/* =========================================
|
| 648 |
-
Metrics & Logs
|
| 649 |
-
========================================= */
|
| 650 |
-
|
| 651 |
-
.metricgrid {
|
| 652 |
-
display: grid;
|
| 653 |
-
grid-template-columns: 1fr 1fr;
|
| 654 |
-
gap: 8px;
|
| 655 |
-
}
|
| 656 |
-
|
| 657 |
-
.metric {
|
| 658 |
-
border: 1px solid rgba(255, 255, 255, .10);
|
| 659 |
-
background: rgba(255, 255, 255, .03);
|
| 660 |
-
border-radius: 14px;
|
| 661 |
-
padding: 10px;
|
| 662 |
-
}
|
| 663 |
-
|
| 664 |
-
.metric .label {
|
| 665 |
-
font-size: 11px;
|
| 666 |
-
color: var(--muted);
|
| 667 |
-
letter-spacing: .12em;
|
| 668 |
-
text-transform: uppercase;
|
| 669 |
-
}
|
| 670 |
-
|
| 671 |
-
.metric .value {
|
| 672 |
-
margin-top: 6px;
|
| 673 |
-
font-family: var(--mono);
|
| 674 |
-
font-size: 16px;
|
| 675 |
-
color: rgba(255, 255, 255, .92);
|
| 676 |
-
}
|
| 677 |
-
|
| 678 |
-
.metric .sub {
|
| 679 |
-
margin-top: 4px;
|
| 680 |
-
font-size: 11px;
|
| 681 |
-
color: var(--faint);
|
| 682 |
-
line-height: 1.35;
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
.log {
|
| 686 |
-
font-family: var(--mono);
|
| 687 |
-
font-size: 11px;
|
| 688 |
-
color: rgba(255, 255, 255, .78);
|
| 689 |
-
line-height: 1.45;
|
| 690 |
-
background: rgba(0, 0, 0, .35);
|
| 691 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 692 |
-
border-radius: 14px;
|
| 693 |
-
padding: 10px;
|
| 694 |
-
height: 210px;
|
| 695 |
-
overflow: auto;
|
| 696 |
-
white-space: pre-wrap;
|
| 697 |
-
}
|
| 698 |
-
|
| 699 |
-
.log .t {
|
| 700 |
-
color: rgba(34, 211, 238, .95);
|
| 701 |
-
}
|
| 702 |
-
|
| 703 |
-
.log .w {
|
| 704 |
-
color: rgba(245, 158, 11, .95);
|
| 705 |
-
}
|
| 706 |
-
|
| 707 |
-
.log .e {
|
| 708 |
-
color: rgba(239, 68, 68, .95);
|
| 709 |
-
}
|
| 710 |
-
|
| 711 |
-
.log .g {
|
| 712 |
-
color: rgba(34, 197, 94, .95);
|
| 713 |
-
}
|
| 714 |
-
|
| 715 |
-
/* =========================================
|
| 716 |
-
Tab Specific: Intel + Frame
|
| 717 |
-
========================================= */
|
| 718 |
-
|
| 719 |
-
.frame-grid {
|
| 720 |
-
display: grid;
|
| 721 |
-
grid-template-columns: 1.6fr 0.9fr;
|
| 722 |
-
grid-template-rows: auto auto;
|
| 723 |
-
gap: 12px;
|
| 724 |
-
min-height: 0;
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
/* Video panel on left */
|
| 728 |
-
.frame-grid .panel-monitor {
|
| 729 |
-
grid-column: 1;
|
| 730 |
-
grid-row: 1 / 3;
|
| 731 |
-
}
|
| 732 |
-
|
| 733 |
-
/* Track cards on right, spanning full height */
|
| 734 |
-
.frame-grid .panel-summary {
|
| 735 |
-
grid-column: 2;
|
| 736 |
-
grid-row: 1 / 3;
|
| 737 |
-
}
|
| 738 |
-
|
| 739 |
-
.intel {
|
| 740 |
-
margin-top: 10px;
|
| 741 |
-
display: flex;
|
| 742 |
-
flex-direction: column;
|
| 743 |
-
gap: 8px;
|
| 744 |
-
}
|
| 745 |
-
|
| 746 |
-
.intel-top {
|
| 747 |
-
display: flex;
|
| 748 |
-
align-items: center;
|
| 749 |
-
justify-content: space-between;
|
| 750 |
-
gap: 8px;
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
.thumbrow {
|
| 754 |
-
display: flex;
|
| 755 |
-
gap: 8px;
|
| 756 |
-
}
|
| 757 |
-
|
| 758 |
-
.thumbrow img {
|
| 759 |
-
flex: 1;
|
| 760 |
-
height: 86px;
|
| 761 |
-
object-fit: cover;
|
| 762 |
-
border-radius: 12px;
|
| 763 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 764 |
-
background: rgba(0, 0, 0, .25);
|
| 765 |
-
}
|
| 766 |
-
|
| 767 |
-
.intelbox {
|
| 768 |
-
font-size: 12px;
|
| 769 |
-
line-height: 1.45;
|
| 770 |
-
color: rgba(255, 255, 255, .84);
|
| 771 |
-
background: rgba(0, 0, 0, .35);
|
| 772 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 773 |
-
border-radius: 14px;
|
| 774 |
-
padding: 10px;
|
| 775 |
-
min-height: 72px;
|
| 776 |
-
}
|
| 777 |
-
|
| 778 |
-
/* =========================================
|
| 779 |
-
Tab Specific: Engage
|
| 780 |
-
========================================= */
|
| 781 |
-
|
| 782 |
-
.engage-grid {
|
| 783 |
-
display: grid;
|
| 784 |
-
grid-template-columns: 1.6fr .9fr;
|
| 785 |
-
gap: 12px;
|
| 786 |
-
min-height: 0;
|
| 787 |
-
transition: grid-template-columns 0.3s ease;
|
| 788 |
-
}
|
| 789 |
-
|
| 790 |
-
.engage-grid.sidebar-collapsed {
|
| 791 |
-
grid-template-columns: 1fr 0fr;
|
| 792 |
-
}
|
| 793 |
-
|
| 794 |
-
.engage-grid.sidebar-collapsed .engage-right {
|
| 795 |
-
display: none;
|
| 796 |
-
}
|
| 797 |
-
|
| 798 |
-
.engage-right {
|
| 799 |
-
display: flex;
|
| 800 |
-
flex-direction: column;
|
| 801 |
-
gap: 12px;
|
| 802 |
-
min-height: 0;
|
| 803 |
-
}
|
| 804 |
-
|
| 805 |
-
.radar {
|
| 806 |
-
height: 540px;
|
| 807 |
-
display: flex;
|
| 808 |
-
flex-direction: column;
|
| 809 |
-
}
|
| 810 |
-
|
| 811 |
-
.radar canvas {
|
| 812 |
-
flex: 1;
|
| 813 |
-
width: 100%;
|
| 814 |
-
height: 100%;
|
| 815 |
-
display: block;
|
| 816 |
-
}
|
| 817 |
-
|
| 818 |
-
.strip {
|
| 819 |
-
display: flex;
|
| 820 |
-
gap: 8px;
|
| 821 |
-
flex-wrap: wrap;
|
| 822 |
-
align-items: center;
|
| 823 |
-
font-size: 12px;
|
| 824 |
-
color: var(--muted);
|
| 825 |
-
}
|
| 826 |
-
|
| 827 |
-
.strip .chip {
|
| 828 |
-
padding: 6px 10px;
|
| 829 |
-
border-radius: 999px;
|
| 830 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 831 |
-
background: rgba(255, 255, 255, .03);
|
| 832 |
-
font-family: var(--mono);
|
| 833 |
-
color: rgba(255, 255, 255, .78);
|
| 834 |
-
}
|
| 835 |
-
|
| 836 |
-
/* Sidebar Checkbox Row */
|
| 837 |
-
.checkbox-row {
|
| 838 |
-
grid-column: span 2;
|
| 839 |
-
margin-top: 8px;
|
| 840 |
-
border-top: 1px solid var(--stroke2);
|
| 841 |
-
padding-top: 8px;
|
| 842 |
-
display: flex;
|
| 843 |
-
align-items: center;
|
| 844 |
-
gap: 8px;
|
| 845 |
-
cursor: pointer;
|
| 846 |
-
}
|
| 847 |
-
|
| 848 |
-
.checkbox-row input[type="checkbox"] {
|
| 849 |
-
width: auto;
|
| 850 |
-
margin: 0;
|
| 851 |
-
}
|
| 852 |
-
|
| 853 |
-
.bar {
|
| 854 |
-
height: 10px;
|
| 855 |
-
border-radius: 999px;
|
| 856 |
-
background: rgba(255, 255, 255, .08);
|
| 857 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 858 |
-
overflow: hidden;
|
| 859 |
-
}
|
| 860 |
-
|
| 861 |
-
.bar>div {
|
| 862 |
-
height: 100%;
|
| 863 |
-
width: 0%;
|
| 864 |
-
background: linear-gradient(90deg, rgba(34, 211, 238, .95), rgba(124, 58, 237, .95));
|
| 865 |
-
transition: width .18s ease;
|
| 866 |
-
}
|
| 867 |
-
|
| 868 |
-
/* =========================================
|
| 869 |
-
Tab Specific: Trade Space
|
| 870 |
-
========================================= */
|
| 871 |
-
|
| 872 |
-
.trade-grid {
|
| 873 |
-
display: grid;
|
| 874 |
-
grid-template-columns: 1.35fr .65fr;
|
| 875 |
-
gap: 12px;
|
| 876 |
-
min-height: 0;
|
| 877 |
-
}
|
| 878 |
-
|
| 879 |
-
.plot {
|
| 880 |
-
height: 420px;
|
| 881 |
-
}
|
| 882 |
-
|
| 883 |
-
/* =========================================
|
| 884 |
-
Utilities
|
| 885 |
-
========================================= */
|
| 886 |
-
|
| 887 |
-
::-webkit-scrollbar {
|
| 888 |
-
width: 10px;
|
| 889 |
-
height: 10px;
|
| 890 |
-
}
|
| 891 |
-
|
| 892 |
-
::-webkit-scrollbar-thumb {
|
| 893 |
-
background: rgba(255, 255, 255, .10);
|
| 894 |
-
border-radius: 999px;
|
| 895 |
-
border: 2px solid rgba(0, 0, 0, .25);
|
| 896 |
-
}
|
| 897 |
-
|
| 898 |
-
::-webkit-scrollbar-thumb:hover {
|
| 899 |
-
background: rgba(255, 255, 255, .16);
|
| 900 |
-
}
|
| 901 |
-
|
| 902 |
-
/* Track Cards */
|
| 903 |
-
.track-card {
|
| 904 |
-
background: rgba(255, 255, 255, 0.025);
|
| 905 |
-
border: 1px solid rgba(255, 255, 255, .10);
|
| 906 |
-
border-radius: 10px;
|
| 907 |
-
padding: 10px 12px;
|
| 908 |
-
margin-bottom: 0;
|
| 909 |
-
cursor: pointer;
|
| 910 |
-
transition: all 0.15s ease;
|
| 911 |
-
}
|
| 912 |
-
|
| 913 |
-
.track-card:hover {
|
| 914 |
-
background: rgba(255, 255, 255, 0.06);
|
| 915 |
-
border-color: rgba(255, 255, 255, .18);
|
| 916 |
-
}
|
| 917 |
-
|
| 918 |
-
.track-card.active {
|
| 919 |
-
border-color: rgba(124, 58, 237, .55);
|
| 920 |
-
background: linear-gradient(135deg, rgba(124, 58, 237, .12), rgba(34, 211, 238, .06));
|
| 921 |
-
box-shadow: 0 0 0 2px rgba(124, 58, 237, .15);
|
| 922 |
-
}
|
| 923 |
-
|
| 924 |
-
.track-card-header {
|
| 925 |
-
display: flex;
|
| 926 |
-
justify-content: space-between;
|
| 927 |
-
align-items: center;
|
| 928 |
-
font-weight: 600;
|
| 929 |
-
margin-bottom: 6px;
|
| 930 |
-
font-size: 12px;
|
| 931 |
-
color: rgba(255, 255, 255, .92);
|
| 932 |
-
letter-spacing: .02em;
|
| 933 |
-
}
|
| 934 |
-
|
| 935 |
-
.track-card-meta {
|
| 936 |
-
font-size: 10px;
|
| 937 |
-
font-family: var(--mono);
|
| 938 |
-
color: rgba(255, 255, 255, .50);
|
| 939 |
-
margin-bottom: 6px;
|
| 940 |
-
letter-spacing: .03em;
|
| 941 |
-
}
|
| 942 |
-
|
| 943 |
-
.track-card-body {
|
| 944 |
-
font-size: 11px;
|
| 945 |
-
line-height: 1.45;
|
| 946 |
-
color: rgba(255, 255, 255, .72);
|
| 947 |
-
background: rgba(0, 0, 0, 0.25);
|
| 948 |
-
padding: 8px 10px;
|
| 949 |
-
border-radius: 8px;
|
| 950 |
-
border: 1px solid rgba(255, 255, 255, .05);
|
| 951 |
-
}
|
| 952 |
-
|
| 953 |
-
.badgemini {
|
| 954 |
-
font-size: 10px;
|
| 955 |
-
font-weight: 600;
|
| 956 |
-
padding: 2px 6px;
|
| 957 |
-
border-radius: 6px;
|
| 958 |
-
letter-spacing: .03em;
|
| 959 |
-
line-height: 1;
|
| 960 |
-
}
|
| 961 |
-
|
| 962 |
-
.track-card-features {
|
| 963 |
-
margin-top: 8px;
|
| 964 |
-
border-top: 1px solid rgba(255, 255, 255, .06);
|
| 965 |
-
padding-top: 8px;
|
| 966 |
-
display: grid;
|
| 967 |
-
grid-template-columns: 1fr 1fr;
|
| 968 |
-
gap: 4px 12px;
|
| 969 |
-
}
|
| 970 |
-
|
| 971 |
-
.track-card-features .feat-row {
|
| 972 |
-
display: flex;
|
| 973 |
-
justify-content: space-between;
|
| 974 |
-
align-items: baseline;
|
| 975 |
-
font-size: 10px;
|
| 976 |
-
padding: 2px 0;
|
| 977 |
-
}
|
| 978 |
-
|
| 979 |
-
.track-card-features .feat-key {
|
| 980 |
-
font-family: var(--mono);
|
| 981 |
-
color: rgba(255, 255, 255, .50);
|
| 982 |
-
letter-spacing: .02em;
|
| 983 |
-
white-space: nowrap;
|
| 984 |
-
margin-right: 6px;
|
| 985 |
-
}
|
| 986 |
-
|
| 987 |
-
.track-card-features .feat-val {
|
| 988 |
-
color: rgba(255, 255, 255, .85);
|
| 989 |
-
text-align: right;
|
| 990 |
-
font-size: 10px;
|
| 991 |
-
}
|
| 992 |
-
|
| 993 |
-
.gpt-badge {
|
| 994 |
-
color: gold;
|
| 995 |
-
font-size: 10px;
|
| 996 |
-
border: 1px solid gold;
|
| 997 |
-
border-radius: 3px;
|
| 998 |
-
padding: 1px 4px;
|
| 999 |
-
margin-left: 6px;
|
| 1000 |
-
}
|
| 1001 |
-
|
| 1002 |
-
.gpt-text {
|
| 1003 |
-
color: rgba(255, 255, 255, .78);
|
| 1004 |
-
}
|
| 1005 |
-
|
| 1006 |
-
/* =========================================
|
| 1007 |
-
Threat Chat Panel
|
| 1008 |
-
========================================= */
|
| 1009 |
-
|
| 1010 |
-
.panel-chat {
|
| 1011 |
-
grid-column: 1 / -1;
|
| 1012 |
-
grid-row: 3;
|
| 1013 |
-
max-height: 260px;
|
| 1014 |
-
display: flex;
|
| 1015 |
-
flex-direction: column;
|
| 1016 |
-
}
|
| 1017 |
-
|
| 1018 |
-
.panel-chat.collapsed .chat-container {
|
| 1019 |
-
display: none;
|
| 1020 |
-
}
|
| 1021 |
-
|
| 1022 |
-
.panel-chat.collapsed {
|
| 1023 |
-
max-height: 44px;
|
| 1024 |
-
}
|
| 1025 |
-
|
| 1026 |
-
.chat-container {
|
| 1027 |
-
display: flex;
|
| 1028 |
-
flex-direction: column;
|
| 1029 |
-
flex: 1;
|
| 1030 |
-
min-height: 0;
|
| 1031 |
-
gap: 8px;
|
| 1032 |
-
}
|
| 1033 |
-
|
| 1034 |
-
.chat-messages {
|
| 1035 |
-
flex: 1;
|
| 1036 |
-
overflow-y: auto;
|
| 1037 |
-
background: rgba(0, 0, 0, .35);
|
| 1038 |
-
border: 1px solid rgba(255, 255, 255, .10);
|
| 1039 |
-
border-radius: 12px;
|
| 1040 |
-
padding: 10px;
|
| 1041 |
-
display: flex;
|
| 1042 |
-
flex-direction: column;
|
| 1043 |
-
gap: 8px;
|
| 1044 |
-
min-height: 100px;
|
| 1045 |
-
max-height: 160px;
|
| 1046 |
-
}
|
| 1047 |
-
|
| 1048 |
-
.chat-message {
|
| 1049 |
-
display: flex;
|
| 1050 |
-
gap: 8px;
|
| 1051 |
-
padding: 8px 10px;
|
| 1052 |
-
border-radius: 10px;
|
| 1053 |
-
font-size: 12px;
|
| 1054 |
-
line-height: 1.4;
|
| 1055 |
-
animation: fadeIn 0.2s ease;
|
| 1056 |
-
}
|
| 1057 |
-
|
| 1058 |
-
@keyframes fadeIn {
|
| 1059 |
-
from {
|
| 1060 |
-
opacity: 0;
|
| 1061 |
-
transform: translateY(4px);
|
| 1062 |
-
}
|
| 1063 |
-
|
| 1064 |
-
to {
|
| 1065 |
-
opacity: 1;
|
| 1066 |
-
transform: translateY(0);
|
| 1067 |
-
}
|
| 1068 |
-
}
|
| 1069 |
-
|
| 1070 |
-
.chat-message.chat-user {
|
| 1071 |
-
background: linear-gradient(135deg, rgba(124, 58, 237, .25), rgba(34, 211, 238, .10));
|
| 1072 |
-
border: 1px solid rgba(124, 58, 237, .35);
|
| 1073 |
-
margin-left: 20px;
|
| 1074 |
-
}
|
| 1075 |
-
|
| 1076 |
-
.chat-message.chat-assistant {
|
| 1077 |
-
background: rgba(255, 255, 255, .04);
|
| 1078 |
-
border: 1px solid rgba(255, 255, 255, .10);
|
| 1079 |
-
margin-right: 20px;
|
| 1080 |
-
}
|
| 1081 |
-
|
| 1082 |
-
.chat-message.chat-system {
|
| 1083 |
-
background: rgba(245, 158, 11, .10);
|
| 1084 |
-
border: 1px solid rgba(245, 158, 11, .25);
|
| 1085 |
-
color: rgba(245, 158, 11, .95);
|
| 1086 |
-
font-size: 11px;
|
| 1087 |
-
}
|
| 1088 |
-
|
| 1089 |
-
.chat-message.loading .chat-content::after {
|
| 1090 |
-
content: "";
|
| 1091 |
-
display: inline-block;
|
| 1092 |
-
width: 12px;
|
| 1093 |
-
height: 12px;
|
| 1094 |
-
border: 2px solid rgba(255, 255, 255, .3);
|
| 1095 |
-
border-top-color: var(--cyan);
|
| 1096 |
-
border-radius: 50%;
|
| 1097 |
-
animation: spin 0.8s linear infinite;
|
| 1098 |
-
margin-left: 8px;
|
| 1099 |
-
vertical-align: middle;
|
| 1100 |
-
}
|
| 1101 |
-
|
| 1102 |
-
.chat-icon {
|
| 1103 |
-
flex-shrink: 0;
|
| 1104 |
-
font-size: 14px;
|
| 1105 |
-
}
|
| 1106 |
-
|
| 1107 |
-
.chat-content {
|
| 1108 |
-
flex: 1;
|
| 1109 |
-
color: rgba(255, 255, 255, .88);
|
| 1110 |
-
word-break: break-word;
|
| 1111 |
-
}
|
| 1112 |
-
|
| 1113 |
-
.chat-input-row {
|
| 1114 |
-
display: flex;
|
| 1115 |
-
gap: 8px;
|
| 1116 |
-
}
|
| 1117 |
-
|
| 1118 |
-
.chat-input-row input {
|
| 1119 |
-
flex: 1;
|
| 1120 |
-
background: rgba(255, 255, 255, .04);
|
| 1121 |
-
border: 1px solid rgba(255, 255, 255, .12);
|
| 1122 |
-
border-radius: 10px;
|
| 1123 |
-
padding: 10px 12px;
|
| 1124 |
-
color: var(--text);
|
| 1125 |
-
font-size: 12px;
|
| 1126 |
-
outline: none;
|
| 1127 |
-
}
|
| 1128 |
-
|
| 1129 |
-
.chat-input-row input:focus {
|
| 1130 |
-
border-color: rgba(124, 58, 237, .55);
|
| 1131 |
-
box-shadow: 0 0 0 3px rgba(124, 58, 237, .16);
|
| 1132 |
-
}
|
| 1133 |
-
|
| 1134 |
-
.chat-input-row input:disabled {
|
| 1135 |
-
opacity: 0.5;
|
| 1136 |
-
cursor: not-allowed;
|
| 1137 |
-
}
|
| 1138 |
-
|
| 1139 |
-
.chat-input-row .btn {
|
| 1140 |
-
padding: 10px 16px;
|
| 1141 |
-
min-width: 70px;
|
| 1142 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index.html
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Detection Base</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body { font-family: system-ui, sans-serif; background: #111; color: #eee; padding: 24px; max-width: 960px; margin: 0 auto; }
|
| 10 |
+
h1 { margin-bottom: 16px; font-size: 1.4em; }
|
| 11 |
+
label { display: block; margin-top: 12px; font-size: 0.85em; color: #aaa; }
|
| 12 |
+
select, input[type="file"], input[type="number"] { width: 100%; padding: 8px; margin-top: 4px; background: #222; color: #eee; border: 1px solid #444; border-radius: 4px; }
|
| 13 |
+
.row { display: flex; gap: 12px; flex-wrap: wrap; }
|
| 14 |
+
.row > div { flex: 1; min-width: 140px; }
|
| 15 |
+
button { margin-top: 16px; padding: 10px 24px; background: #2563eb; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-size: 1em; }
|
| 16 |
+
button:disabled { background: #555; cursor: not-allowed; }
|
| 17 |
+
#status { margin-top: 12px; padding: 8px; font-size: 0.9em; color: #aaa; }
|
| 18 |
+
.preview { margin-top: 16px; }
|
| 19 |
+
.preview img, .preview video { max-width: 100%; border-radius: 4px; border: 1px solid #333; }
|
| 20 |
+
#detections { margin-top: 12px; background: #1a1a1a; padding: 12px; border-radius: 4px; max-height: 300px; overflow: auto; font-family: monospace; font-size: 0.8em; white-space: pre-wrap; display: none; }
|
| 21 |
+
.toggle { margin-top: 8px; font-size: 0.85em; color: #6ba3ff; cursor: pointer; text-decoration: underline; }
|
| 22 |
+
.hidden { display: none; }
|
| 23 |
+
#segOpts { display: none; }
|
| 24 |
+
</style>
|
| 25 |
+
</head>
|
| 26 |
+
<body>
|
| 27 |
+
<h1>Detection Base</h1>
|
| 28 |
+
|
| 29 |
+
<label for="video">Video</label>
|
| 30 |
+
<input type="file" id="video" accept="video/*">
|
| 31 |
+
|
| 32 |
+
<div class="row">
|
| 33 |
+
<div>
|
| 34 |
+
<label>Mode</label>
|
| 35 |
+
<select id="mode">
|
| 36 |
+
<option value="object_detection">Object Detection</option>
|
| 37 |
+
<option value="segmentation">Segmentation</option>
|
| 38 |
+
<option value="drone_detection">Drone Detection</option>
|
| 39 |
+
</select>
|
| 40 |
+
</div>
|
| 41 |
+
<div>
|
| 42 |
+
<label>Detector</label>
|
| 43 |
+
<select id="detector">
|
| 44 |
+
<option value="yolo11">YOLO11</option>
|
| 45 |
+
<option value="detr_resnet50">DETR</option>
|
| 46 |
+
<option value="grounding_dino">Grounding DINO</option>
|
| 47 |
+
<option value="drone_yolo">Drone YOLO</option>
|
| 48 |
+
</select>
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
|
| 52 |
+
<div id="segOpts" class="row">
|
| 53 |
+
<div>
|
| 54 |
+
<label>Segmenter</label>
|
| 55 |
+
<select id="segmenter">
|
| 56 |
+
<option value="GSAM2-L">GSAM2-L</option>
|
| 57 |
+
<option value="GSAM2-B">GSAM2-B</option>
|
| 58 |
+
<option value="GSAM2-S">GSAM2-S</option>
|
| 59 |
+
<option value="YSAM2-L">YSAM2-L</option>
|
| 60 |
+
<option value="YSAM2-B">YSAM2-B</option>
|
| 61 |
+
<option value="YSAM2-S">YSAM2-S</option>
|
| 62 |
+
</select>
|
| 63 |
+
</div>
|
| 64 |
+
<div>
|
| 65 |
+
<label>Step (keyframe interval)</label>
|
| 66 |
+
<input type="number" id="step" value="7" min="1" max="60">
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<label>Queries (comma-separated, optional)</label>
|
| 71 |
+
<input type="text" id="queries" placeholder="person, car, truck" style="width:100%;padding:8px;margin-top:4px;background:#222;color:#eee;border:1px solid #444;border-radius:4px;">
|
| 72 |
+
|
| 73 |
+
<div class="row">
|
| 74 |
+
<div>
|
| 75 |
+
<label><input type="checkbox" id="enableDepth"> Enable depth estimation</label>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<button id="submit" onclick="submit()">Run Detection</button>
|
| 80 |
+
<div id="status"></div>
|
| 81 |
+
|
| 82 |
+
<div class="preview">
|
| 83 |
+
<div id="firstFrameBox" class="hidden">
|
| 84 |
+
<p>First Frame</p>
|
| 85 |
+
<img id="firstFrame" alt="First frame detection preview">
|
| 86 |
+
</div>
|
| 87 |
+
<div id="streamBox" class="hidden">
|
| 88 |
+
<p>Live Stream</p>
|
| 89 |
+
<img id="stream" alt="Live MJPEG detection stream">
|
| 90 |
+
</div>
|
| 91 |
+
<div id="videoBox" class="hidden">
|
| 92 |
+
<p>Processed Video</p>
|
| 93 |
+
<video id="result" controls type="video/mp4"></video>
|
| 94 |
+
</div>
|
| 95 |
+
</div>
|
| 96 |
+
<span class="toggle hidden" id="detToggle" onclick="toggleDets()">Show detections JSON</span>
|
| 97 |
+
<pre id="detections"></pre>
|
| 98 |
+
|
| 99 |
+
<script>
|
| 100 |
+
const $ = id => document.getElementById(id);
|
| 101 |
+
|
| 102 |
+
$('mode').onchange = () => {
|
| 103 |
+
const seg = $('mode').value === 'segmentation';
|
| 104 |
+
$('segOpts').style.display = seg ? 'flex' : 'none';
|
| 105 |
+
$('detector').parentElement.style.display = seg ? 'none' : '';
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
let pollTimer = null;
|
| 109 |
+
|
| 110 |
+
async function submit() {
|
| 111 |
+
const file = $('video').files[0];
|
| 112 |
+
if (!file) return alert('Select a video file');
|
| 113 |
+
|
| 114 |
+
$('submit').disabled = true;
|
| 115 |
+
$('status').textContent = 'Uploading...';
|
| 116 |
+
hide('firstFrameBox'); hide('streamBox'); hide('videoBox'); hide('detToggle');
|
| 117 |
+
$('detections').style.display = 'none';
|
| 118 |
+
|
| 119 |
+
const fd = new FormData();
|
| 120 |
+
fd.append('video', file);
|
| 121 |
+
fd.append('mode', $('mode').value);
|
| 122 |
+
fd.append('queries', $('queries').value);
|
| 123 |
+
fd.append('detector', $('detector').value);
|
| 124 |
+
fd.append('segmenter', $('segmenter').value);
|
| 125 |
+
fd.append('step', $('step').value);
|
| 126 |
+
fd.append('enable_depth', $('enableDepth').checked);
|
| 127 |
+
|
| 128 |
+
try {
|
| 129 |
+
const res = await fetch('/detect/async', { method: 'POST', body: fd });
|
| 130 |
+
if (!res.ok) { const e = await res.json(); throw new Error(e.detail || res.statusText); }
|
| 131 |
+
const data = await res.json();
|
| 132 |
+
|
| 133 |
+
$('status').textContent = 'Processing...';
|
| 134 |
+
|
| 135 |
+
// Show first frame
|
| 136 |
+
$('firstFrame').src = data.first_frame_url;
|
| 137 |
+
show('firstFrameBox');
|
| 138 |
+
|
| 139 |
+
// Show detections
|
| 140 |
+
if (data.first_frame_detections && data.first_frame_detections.length) {
|
| 141 |
+
$('detections').textContent = JSON.stringify(data.first_frame_detections, null, 2);
|
| 142 |
+
show('detToggle');
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
// Start stream
|
| 146 |
+
$('stream').src = data.stream_url;
|
| 147 |
+
show('streamBox');
|
| 148 |
+
|
| 149 |
+
// Poll for completion
|
| 150 |
+
pollTimer = setInterval(async () => {
|
| 151 |
+
try {
|
| 152 |
+
const sr = await fetch(data.status_url);
|
| 153 |
+
const st = await sr.json();
|
| 154 |
+
$('status').textContent = `Status: ${st.status}`;
|
| 155 |
+
|
| 156 |
+
if (st.status === 'completed') {
|
| 157 |
+
clearInterval(pollTimer);
|
| 158 |
+
hide('streamBox');
|
| 159 |
+
$('result').src = data.video_url;
|
| 160 |
+
show('videoBox');
|
| 161 |
+
$('submit').disabled = false;
|
| 162 |
+
// Update detections with final data
|
| 163 |
+
if (st.first_frame_detections && st.first_frame_detections.length) {
|
| 164 |
+
$('detections').textContent = JSON.stringify(st.first_frame_detections, null, 2);
|
| 165 |
+
show('detToggle');
|
| 166 |
+
}
|
| 167 |
+
} else if (st.status === 'failed') {
|
| 168 |
+
clearInterval(pollTimer);
|
| 169 |
+
$('status').textContent = `Failed: ${st.error}`;
|
| 170 |
+
$('submit').disabled = false;
|
| 171 |
+
} else if (st.status === 'cancelled') {
|
| 172 |
+
clearInterval(pollTimer);
|
| 173 |
+
$('status').textContent = 'Cancelled';
|
| 174 |
+
$('submit').disabled = false;
|
| 175 |
+
}
|
| 176 |
+
} catch(e) { /* keep polling */ }
|
| 177 |
+
}, 2000);
|
| 178 |
+
|
| 179 |
+
} catch(e) {
|
| 180 |
+
$('status').textContent = `Error: ${e.message}`;
|
| 181 |
+
$('submit').disabled = false;
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
function show(id) { $(id).classList.remove('hidden'); }
|
| 186 |
+
function hide(id) {
|
| 187 |
+
$(id).classList.add('hidden');
|
| 188 |
+
const img = $(id).querySelector('img');
|
| 189 |
+
if (img && img.src) img.src = '';
|
| 190 |
+
}
|
| 191 |
+
function toggleDets() {
|
| 192 |
+
const d = $('detections');
|
| 193 |
+
d.style.display = d.style.display === 'none' ? 'block' : 'none';
|
| 194 |
+
}
|
| 195 |
+
</script>
|
| 196 |
+
</body>
|
| 197 |
+
</html>
|