trysem commited on
Commit
110164f
·
verified ·
1 Parent(s): ca66302

Create videoflow

Browse files
Files changed (1) hide show
  1. videoflow +661 -0
videoflow ADDED
@@ -0,0 +1,661 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>HumanFrame AI | Local Video Extractor</title>
7
+ <!-- Tailwind -->
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <!-- JSZip for exporting -->
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
11
+ <!-- Icons -->
12
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
13
+ <!-- Face API for Facial Recognition -->
14
+ <script src="https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/dist/face-api.js"></script>
15
+
16
+ <style>
17
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
18
+
19
+ body { font-family: 'Inter', sans-serif; }
20
+
21
+ .glass-panel {
22
+ background: rgba(255, 255, 255, 0.03);
23
+ backdrop-filter: blur(12px);
24
+ border: 1px solid rgba(255, 255, 255, 0.08);
25
+ }
26
+
27
+ .gradient-text {
28
+ background: linear-gradient(135deg, #818cf8 0%, #c084fc 100%);
29
+ -webkit-background-clip: text;
30
+ -webkit-text-fill-color: transparent;
31
+ }
32
+
33
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
34
+
35
+ #results-container::-webkit-scrollbar { width: 5px; }
36
+ #results-container::-webkit-scrollbar-track { background: transparent; }
37
+ #results-container::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }
38
+
39
+ .scan-line {
40
+ position: absolute; top: 0; left: 0; width: 100%; height: 2px;
41
+ background: #818cf8; box-shadow: 0 0 15px #818cf8;
42
+ animation: scan 2s linear infinite; display: none; z-index: 10;
43
+ }
44
+
45
+ @keyframes scan { 0% { top: 0%; } 100% { top: 100%; } }
46
+
47
+ /* Toggle Switch CSS */
48
+ .toggle-checkbox:checked { right: 0; border-color: #6366f1; }
49
+ .toggle-checkbox:checked + .toggle-label { background-color: #6366f1; }
50
+ .toggle-checkbox { right: 4px; z-index: 1; border-color: #e2e8f0; transition: all 0.3s; }
51
+ .toggle-label { background-color: #cbd5e1; transition: all 0.3s; }
52
+ </style>
53
+ </head>
54
+ <body class="bg-[#0f172a] text-slate-200 min-h-screen selection:bg-indigo-500/30">
55
+
56
+ <!-- Navbar -->
57
+ <nav class="border-b border-white/5 bg-[#0f172a]/80 backdrop-blur-md sticky top-0 z-50">
58
+ <div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
59
+ <div class="flex items-center gap-3">
60
+ <div class="w-10 h-10 bg-indigo-600 rounded-xl flex items-center justify-center shadow-lg shadow-indigo-500/20">
61
+ <i class="fas fa-user-check text-white text-lg"></i>
62
+ </div>
63
+ <h1 class="text-xl font-bold tracking-tight">HumanFrame<span class="gradient-text">AI</span></h1>
64
+ </div>
65
+
66
+ <div class="flex gap-3">
67
+ <div id="model-badge" class="flex items-center gap-2 bg-slate-800/50 px-3 py-1.5 rounded-full border border-white/5 text-xs font-medium text-slate-400">
68
+ <span class="status-dot bg-amber-500 animate-pulse"></span> MediaPipe Loading...
69
+ </div>
70
+ <div id="face-badge" class="flex items-center gap-2 bg-slate-800/50 px-3 py-1.5 rounded-full border border-white/5 text-xs font-medium text-slate-400 hidden">
71
+ <span class="status-dot bg-amber-500 animate-pulse"></span> FaceAPI Loading...
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </nav>
76
+
77
+ <main class="max-w-7xl mx-auto px-6 py-10">
78
+ <div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
79
+
80
+ <!-- Left Panel: Configuration -->
81
+ <div class="lg:col-span-4 space-y-6">
82
+
83
+ <!-- Upload Zone -->
84
+ <div id="drop-zone" class="glass-panel rounded-2xl p-8 text-center border-2 border-dashed border-indigo-500/20 hover:border-indigo-500/50 transition-all cursor-pointer group relative overflow-hidden">
85
+ <input type="file" id="video-input" class="hidden" accept="video/*">
86
+ <div class="relative z-10">
87
+ <div class="w-14 h-14 bg-indigo-500/10 rounded-2xl flex items-center justify-center mx-auto mb-3 group-hover:scale-110 transition-transform duration-300">
88
+ <i class="fas fa-video text-indigo-400 text-xl"></i>
89
+ </div>
90
+ <h3 class="text-md font-semibold text-white mb-1">Upload Video</h3>
91
+ <p class="text-xs text-slate-400">Drag & drop or click to browse</p>
92
+ </div>
93
+ </div>
94
+
95
+ <!-- Settings & Advanced -->
96
+ <div id="settings-panel" class="glass-panel rounded-2xl p-6 space-y-6 opacity-40 pointer-events-none transition-all">
97
+
98
+ <!-- Basic Engine -->
99
+ <div class="space-y-4">
100
+ <div class="flex items-center justify-between border-b border-white/5 pb-2">
101
+ <h4 class="font-bold text-white text-xs uppercase tracking-wider">Engine Settings</h4>
102
+ </div>
103
+
104
+ <div class="space-y-2">
105
+ <label class="text-xs font-semibold text-slate-400 flex justify-between">
106
+ Scan Interval
107
+ <span id="interval-val" class="text-indigo-400">0.5s</span>
108
+ </label>
109
+ <input type="range" id="scan-rate" min="0.1" max="2.0" step="0.1" value="0.5" class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500">
110
+ </div>
111
+
112
+ <div class="space-y-2">
113
+ <label class="text-xs font-semibold text-slate-400 flex justify-between">
114
+ Detection Confidence
115
+ <span id="conf-val" class="text-indigo-400">50%</span>
116
+ </label>
117
+ <input type="range" id="confidence" min="0.3" max="0.9" step="0.05" value="0.5" class="w-full h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500">
118
+ </div>
119
+ </div>
120
+
121
+ <!-- Advanced Features -->
122
+ <div class="space-y-4">
123
+ <div class="flex items-center justify-between border-b border-white/5 pb-2">
124
+ <h4 class="font-bold text-white text-xs uppercase tracking-wider text-purple-400">Advanced Extraction</h4>
125
+ </div>
126
+
127
+ <!-- Smart Body Crop Toggle -->
128
+ <div class="flex items-center justify-between">
129
+ <div class="flex flex-col">
130
+ <span class="text-sm font-medium text-slate-200">Smart Body Crop</span>
131
+ <span class="text-[10px] text-slate-500">Extracts the person bounding box</span>
132
+ </div>
133
+ <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
134
+ <input type="checkbox" name="toggle" id="auto-crop-toggle" class="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
135
+ <label for="auto-crop-toggle" class="toggle-label block overflow-hidden h-5 rounded-full bg-slate-600 cursor-pointer"></label>
136
+ </div>
137
+ </div>
138
+
139
+ <!-- 512x512 Face Crop Toggle -->
140
+ <div class="flex items-center justify-between">
141
+ <div class="flex flex-col">
142
+ <span class="text-sm font-medium text-slate-200">Tight Face Crop (512px)</span>
143
+ <span class="text-[10px] text-slate-500">Extracts faces specifically in 512x512</span>
144
+ </div>
145
+ <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
146
+ <input type="checkbox" name="toggle" id="face-crop-toggle" class="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
147
+ <label for="face-crop-toggle" class="toggle-label block overflow-hidden h-5 rounded-full bg-slate-600 cursor-pointer"></label>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Require Visible Face Toggle -->
152
+ <div class="flex items-center justify-between">
153
+ <div class="flex flex-col">
154
+ <span class="text-sm font-medium text-slate-200">Require Visible Face</span>
155
+ <span class="text-[10px] text-slate-500">Skip frames with bodies but no faces</span>
156
+ </div>
157
+ <div class="relative inline-block w-10 mr-2 align-middle select-none transition duration-200 ease-in">
158
+ <input type="checkbox" name="toggle" id="require-face-toggle" class="toggle-checkbox absolute block w-5 h-5 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
159
+ <label for="require-face-toggle" class="toggle-label block overflow-hidden h-5 rounded-full bg-slate-600 cursor-pointer"></label>
160
+ </div>
161
+ </div>
162
+
163
+ <!-- Target Face Match -->
164
+ <div class="bg-slate-800/40 p-3 rounded-xl border border-white/5">
165
+ <div class="flex flex-col mb-2">
166
+ <span class="text-sm font-medium text-slate-200">Target Face Match</span>
167
+ <span class="text-[10px] text-slate-500">Only extract frames containing this person</span>
168
+ </div>
169
+
170
+ <div class="flex items-center gap-3 mt-3">
171
+ <div id="face-upload-btn" class="flex-grow bg-slate-700 hover:bg-slate-600 text-xs text-center py-2 rounded-lg cursor-pointer transition-colors border border-dashed border-slate-500">
172
+ <i class="fas fa-camera mr-1"></i> Upload Target Face
173
+ </div>
174
+ <input type="file" id="face-input" class="hidden" accept="image/*">
175
+ <img id="target-face-preview" class="hidden w-10 h-10 object-cover rounded-full border-2 border-indigo-500 shadow-[0_0_10px_rgba(99,102,241,0.5)]">
176
+ </div>
177
+ <p id="face-status-text" class="text-[10px] text-amber-400 mt-2 hidden text-center"><i class="fas fa-spinner animate-spin"></i> Analyzing face...</p>
178
+ </div>
179
+ </div>
180
+
181
+ <button id="start-btn" class="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 rounded-xl shadow-xl shadow-indigo-500/10 transition-all flex items-center justify-center gap-3">
182
+ <i class="fas fa-microchip"></i> Start AI Extraction
183
+ </button>
184
+ </div>
185
+
186
+ <!-- Monitoring Window -->
187
+ <div class="glass-panel rounded-2xl overflow-hidden relative shadow-2xl group border-2 border-[#0f172a]">
188
+ <div class="scan-line" id="scanner"></div>
189
+ <canvas id="preview-canvas" class="w-full aspect-video bg-black object-contain"></canvas>
190
+ <div class="absolute bottom-0 left-0 right-0 p-3 bg-gradient-to-t from-black/80 to-transparent flex items-center justify-between">
191
+ <span class="text-[10px] font-bold tracking-widest text-indigo-400 uppercase">Live Monitor</span>
192
+ <div id="fps-counter" class="text-[10px] font-mono text-slate-400">-- FPS</div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+
197
+ <!-- Right Panel: Results Gallery -->
198
+ <div class="lg:col-span-8 flex flex-col h-[calc(100vh-160px)] min-h-[600px]">
199
+ <div class="glass-panel rounded-3xl flex flex-col h-full overflow-hidden border border-white/10 relative">
200
+
201
+ <!-- Toolbar -->
202
+ <div class="p-6 border-b border-white/5 flex flex-wrap gap-4 items-center justify-between bg-white/[0.02]">
203
+ <div>
204
+ <h2 class="text-xl font-bold text-white">Detection Gallery</h2>
205
+ <p id="stats-text" class="text-sm text-slate-500">System idle. Awaiting video upload.</p>
206
+ </div>
207
+ <div class="flex gap-2">
208
+ <button id="download-btn" class="hidden px-5 py-2.5 bg-emerald-500 hover:bg-emerald-400 text-white text-sm font-bold rounded-xl transition-all flex items-center gap-2 shadow-lg shadow-emerald-500/20">
209
+ <i class="fas fa-file-export"></i> Export All (.zip)
210
+ </button>
211
+ </div>
212
+ </div>
213
+
214
+ <!-- Progress Bar -->
215
+ <div id="progress-container" class="px-6 py-4 bg-indigo-500/5 hidden border-b border-white/5">
216
+ <div class="flex justify-between items-center mb-2">
217
+ <span class="text-xs font-bold text-indigo-300 uppercase tracking-tighter" id="status-label">Analyzing Frames...</span>
218
+ <span class="text-xs font-mono text-indigo-300" id="progress-percent">0%</span>
219
+ </div>
220
+ <div class="w-full bg-white/5 h-1.5 rounded-full overflow-hidden">
221
+ <div id="progress-bar" class="h-full bg-indigo-500 transition-all duration-300 shadow-[0_0_10px_#6366f1]" style="width: 0%"></div>
222
+ </div>
223
+ </div>
224
+
225
+ <!-- Gallery Grid -->
226
+ <div id="results-container" class="flex-grow overflow-y-auto p-6 relative">
227
+ <!-- Auto-masonry/flex grid for mixed crop sizes -->
228
+ <div id="results" class="flex flex-wrap gap-4 content-start">
229
+ <!-- Frames will be injected here -->
230
+ </div>
231
+ <!-- Placeholder -->
232
+ <div id="empty-state" class="absolute inset-0 flex flex-col items-center justify-center opacity-20">
233
+ <i class="fas fa-images text-7xl mb-6"></i>
234
+ <p class="text-lg font-medium">No frames extracted yet</p>
235
+ </div>
236
+ </div>
237
+ </div>
238
+ </div>
239
+ </div>
240
+ </main>
241
+
242
+ <video id="hidden-video" class="hidden" muted></video>
243
+
244
+ <!-- Import MediaPipe -->
245
+ <script type="module">
246
+ import { ObjectDetector, FilesetResolver } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/vision_bundle.mjs";
247
+
248
+ // DOM Elements
249
+ const videoInput = document.getElementById('video-input');
250
+ const dropZone = document.getElementById('drop-zone');
251
+ const videoEl = document.getElementById('hidden-video');
252
+ const previewCanvas = document.getElementById('preview-canvas');
253
+ const resultsEl = document.getElementById('results');
254
+ const progressBar = document.getElementById('progress-bar');
255
+ const progressPercent = document.getElementById('progress-percent');
256
+ const statusLabel = document.getElementById('status-label');
257
+ const startBtn = document.getElementById('start-btn');
258
+ const downloadBtn = document.getElementById('download-btn');
259
+ const modelBadge = document.getElementById('model-badge');
260
+ const faceBadge = document.getElementById('face-badge');
261
+ const settingsPanel = document.getElementById('settings-panel');
262
+ const statsText = document.getElementById('stats-text');
263
+ const scannerLine = document.getElementById('scanner');
264
+ const emptyState = document.getElementById('empty-state');
265
+ const autoCropToggle = document.getElementById('auto-crop-toggle');
266
+ const faceCropToggle = document.getElementById('face-crop-toggle');
267
+ const requireFaceToggle = document.getElementById('require-face-toggle');
268
+
269
+ // Face Match Elements
270
+ const faceUploadBtn = document.getElementById('face-upload-btn');
271
+ const faceInput = document.getElementById('face-input');
272
+ const facePreview = document.getElementById('target-face-preview');
273
+ const faceStatusText = document.getElementById('face-status-text');
274
+
275
+ // State variables
276
+ let detector;
277
+ let extractedFrames = [];
278
+ let isProcessing = false;
279
+ let targetFaceDescriptor = null;
280
+ let isFaceApiLoaded = false;
281
+
282
+ // UI Listeners
283
+ document.getElementById('scan-rate').oninput = (e) => document.getElementById('interval-val').innerText = e.target.value + 's';
284
+ document.getElementById('confidence').oninput = (e) => document.getElementById('conf-val').innerText = Math.round(e.target.value * 100) + '%';
285
+
286
+ // 1. Init MediaPipe Object Detector (Body detection)
287
+ async function initMediaPipe() {
288
+ try {
289
+ const vision = await FilesetResolver.forVisionTasks("https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm");
290
+ detector = await ObjectDetector.createFromOptions(vision, {
291
+ baseOptions: {
292
+ modelAssetPath: `https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite0/float16/1/efficientdet_lite0.tflite`,
293
+ delegate: "GPU"
294
+ },
295
+ scoreThreshold: 0.5,
296
+ runningMode: "IMAGE"
297
+ });
298
+ modelBadge.innerHTML = '<span class="status-dot bg-emerald-500"></span> MediaPipe Ready';
299
+ modelBadge.classList.replace('text-slate-400', 'text-emerald-400');
300
+ } catch (err) {
301
+ console.error("MediaPipe Error:", err);
302
+ modelBadge.innerHTML = '<span class="status-dot bg-red-500"></span> Engine Error';
303
+ }
304
+ }
305
+
306
+ // 2. Init Face-API (Facial Recognition)
307
+ async function initFaceAPI() {
308
+ faceBadge.classList.remove('hidden');
309
+ try {
310
+ const MODEL_URL = 'https://cdn.jsdelivr.net/npm/@vladmandic/face-api@1.7.12/model/';
311
+ await Promise.all([
312
+ faceapi.nets.ssdMobilenetv1.loadFromUri(MODEL_URL),
313
+ faceapi.nets.faceLandmark68Net.loadFromUri(MODEL_URL),
314
+ faceapi.nets.faceRecognitionNet.loadFromUri(MODEL_URL)
315
+ ]);
316
+ isFaceApiLoaded = true;
317
+ faceBadge.innerHTML = '<span class="status-dot bg-purple-500"></span> FaceAPI Ready';
318
+ faceBadge.classList.replace('text-slate-400', 'text-purple-400');
319
+ } catch(err) {
320
+ console.error("FaceAPI Error:", err);
321
+ faceBadge.innerHTML = '<span class="status-dot bg-red-500"></span> FaceAPI Error';
322
+ }
323
+ }
324
+
325
+ // Initialize models in background
326
+ initMediaPipe();
327
+ initFaceAPI();
328
+
329
+ // Target Face Upload Handler
330
+ faceUploadBtn.onclick = () => faceInput.click();
331
+ faceInput.onchange = async (e) => {
332
+ const file = e.target.files[0];
333
+ if (!file) return;
334
+
335
+ if (!isFaceApiLoaded) {
336
+ alert("Please wait for FaceAPI to finish loading...");
337
+ return;
338
+ }
339
+
340
+ faceStatusText.classList.remove('hidden');
341
+ faceStatusText.innerHTML = '<i class="fas fa-spinner animate-spin"></i> Analyzing face...';
342
+ faceStatusText.className = "text-[10px] text-amber-400 mt-2 text-center";
343
+
344
+ try {
345
+ const url = URL.createObjectURL(file);
346
+ facePreview.onload = async () => {
347
+ try {
348
+ // Extract facial blueprint using slightly lower confidence to prevent missed detections
349
+ const options = new faceapi.SsdMobilenetv1Options({ minConfidence: 0.2 });
350
+ const detection = await faceapi.detectSingleFace(facePreview, options).withFaceLandmarks().withFaceDescriptor();
351
+
352
+ if (detection) {
353
+ targetFaceDescriptor = detection.descriptor;
354
+ faceStatusText.innerHTML = '<i class="fas fa-check text-emerald-400"></i> Target Face Locked';
355
+ faceStatusText.className = "text-[10px] text-emerald-400 mt-2 text-center font-bold";
356
+ facePreview.classList.remove('border-red-500');
357
+ } else {
358
+ targetFaceDescriptor = null;
359
+ facePreview.classList.add('border-red-500');
360
+ faceStatusText.innerHTML = '<i class="fas fa-times text-red-500"></i> No face detected. Try another photo.';
361
+ faceStatusText.className = "text-[10px] text-red-500 mt-2 text-center";
362
+ }
363
+ } catch (err) {
364
+ console.error("Detection error:", err);
365
+ faceStatusText.innerHTML = '<i class="fas fa-times text-red-500"></i> Processing error.';
366
+ }
367
+ };
368
+ facePreview.src = url;
369
+ faceUploadBtn.classList.add('hidden');
370
+ facePreview.classList.remove('hidden');
371
+ } catch (err) {
372
+ console.error(err);
373
+ faceStatusText.innerHTML = 'Error reading image.';
374
+ }
375
+ };
376
+
377
+ // Video Upload Handler
378
+ dropZone.onclick = () => videoInput.click();
379
+ videoInput.onchange = (e) => {
380
+ const file = e.target.files[0];
381
+ if (file) {
382
+ settingsPanel.style.opacity = "1";
383
+ settingsPanel.style.pointerEvents = "all";
384
+ dropZone.querySelector('h3').innerText = file.name;
385
+ statsText.innerText = "Video loaded. Ready to scan.";
386
+ }
387
+ };
388
+
389
+ // Extraction Process
390
+ startBtn.onclick = async () => {
391
+ if (isProcessing) {
392
+ isProcessing = false;
393
+ return;
394
+ }
395
+
396
+ const file = videoInput.files[0];
397
+ if (!file || !detector) return;
398
+
399
+ // Setup UI for processing
400
+ isProcessing = true;
401
+ extractedFrames = [];
402
+ resultsEl.innerHTML = '';
403
+
404
+ if (emptyState) emptyState.classList.add('hidden');
405
+ document.getElementById('progress-container').classList.remove('hidden');
406
+ if (scannerLine) scannerLine.style.display = "block";
407
+
408
+ startBtn.innerHTML = '<i class="fas fa-stop"></i> Stop Analysis';
409
+ startBtn.classList.replace('bg-indigo-600', 'bg-red-600');
410
+ downloadBtn.classList.add('hidden');
411
+
412
+ const url = URL.createObjectURL(file);
413
+ videoEl.src = url;
414
+
415
+ videoEl.onloadedmetadata = async () => {
416
+ const ctx = previewCanvas.getContext('2d');
417
+ previewCanvas.width = videoEl.videoWidth;
418
+ previewCanvas.height = videoEl.videoHeight;
419
+
420
+ const duration = videoEl.duration;
421
+ const step = parseFloat(document.getElementById('scan-rate').value);
422
+ const confidence = parseFloat(document.getElementById('confidence').value);
423
+ const doAutoCrop = autoCropToggle.checked;
424
+ const doFaceCrop = faceCropToggle.checked;
425
+ const requireFace = requireFaceToggle.checked;
426
+ const matchFace = targetFaceDescriptor !== null;
427
+
428
+ let lastTime = performance.now();
429
+
430
+ for (let time = 0; time < duration; time += step) {
431
+ if (!isProcessing) break;
432
+
433
+ videoEl.currentTime = time;
434
+ await new Promise(r => videoEl.onseeked = r);
435
+
436
+ // Draw frame
437
+ ctx.drawImage(videoEl, 0, 0);
438
+
439
+ // 1. Detect bodies (MediaPipe)
440
+ detector.setOptions({ scoreThreshold: confidence });
441
+ const results = detector.detect(previewCanvas);
442
+ let people = results.detections.filter(d =>
443
+ d.categories.some(c => c.categoryName === 'person')
444
+ );
445
+
446
+ let validEntities = [];
447
+
448
+ if (people.length > 0) {
449
+
450
+ // 2. Face API Processing (If required by any advanced setting)
451
+ if (matchFace || requireFace || doFaceCrop) {
452
+ // Detect all faces with a reasonable confidence
453
+ const faces = await faceapi.detectAllFaces(previewCanvas, new faceapi.SsdMobilenetv1Options({minConfidence: 0.3})).withFaceLandmarks().withFaceDescriptors();
454
+
455
+ if (matchFace) {
456
+ const matchingFaces = faces.filter(f => faceapi.euclideanDistance(targetFaceDescriptor, f.descriptor) < 0.55);
457
+ matchingFaces.forEach(f => {
458
+ // Try to link to a person body
459
+ let linkedPerson = people.find(p => isFaceInBody(f.detection.box, p.boundingBox));
460
+ validEntities.push({ personBox: linkedPerson ? linkedPerson.boundingBox : null, faceBox: f.detection.box, isMatch: true });
461
+ });
462
+ } else if (requireFace) {
463
+ // Must have a face to be valid
464
+ faces.forEach(f => {
465
+ let linkedPerson = people.find(p => isFaceInBody(f.detection.box, p.boundingBox));
466
+ validEntities.push({ personBox: linkedPerson ? linkedPerson.boundingBox : null, faceBox: f.detection.box, isMatch: false });
467
+ });
468
+ } else {
469
+ // Don't explicitly require face, but want to grab face box if available (for 512x512 crop)
470
+ people.forEach(p => {
471
+ let linkedFace = faces.find(f => isFaceInBody(f.detection.box, p.boundingBox));
472
+ validEntities.push({ personBox: p.boundingBox, faceBox: linkedFace ? linkedFace.detection.box : null, isMatch: false });
473
+ });
474
+ }
475
+ } else {
476
+ // Basic extraction, no face API needed
477
+ people.forEach(p => validEntities.push({ personBox: p.boundingBox, faceBox: null, isMatch: false }));
478
+ }
479
+ }
480
+
481
+ // 3. Save and Crop
482
+ if (validEntities.length > 0) {
483
+
484
+ if (!doAutoCrop && !doFaceCrop) {
485
+ // Save full frame once if no crop is selected
486
+ const fullFrameData = previewCanvas.toDataURL('image/jpeg', 0.9);
487
+ extractedFrames.push({ data: fullFrameData, time: time, type: 'Full' });
488
+ addFrameToUI(fullFrameData, time, 'Full');
489
+ } else {
490
+ // Extract EACH entity based on selected crops
491
+ validEntities.forEach(entity => {
492
+ // Normal Body Crop
493
+ if (doAutoCrop && entity.personBox) {
494
+ const box = entity.personBox;
495
+ const padX = box.width * 0.15;
496
+ const padY = box.height * 0.15;
497
+ const cX = Math.max(0, box.originX - padX);
498
+ const cY = Math.max(0, box.originY - padY);
499
+ const cW = Math.min(previewCanvas.width - cX, box.width + padX * 2);
500
+ const cH = Math.min(previewCanvas.height - cY, box.height + padY * 2);
501
+
502
+ const cropCanvas = document.createElement('canvas');
503
+ cropCanvas.width = cW;
504
+ cropCanvas.height = cH;
505
+ cropCanvas.getContext('2d').drawImage(previewCanvas, cX, cY, cW, cH, 0, 0, cW, cH);
506
+
507
+ const frameData = cropCanvas.toDataURL('image/jpeg', 0.9);
508
+ extractedFrames.push({ data: frameData, time: time, type: 'Body' });
509
+ addFrameToUI(frameData, time, 'Body');
510
+ }
511
+
512
+ // Tight Face Crop (512x512)
513
+ if (doFaceCrop && entity.faceBox) {
514
+ const fBox = entity.faceBox;
515
+ // Make crop region ~2x face size
516
+ const size = Math.max(fBox.width, fBox.height) * 2.0;
517
+ const centerX = fBox.x + fBox.width / 2;
518
+ const centerY = fBox.y + fBox.height / 2;
519
+
520
+ const sX = Math.max(0, centerX - size / 2);
521
+ const sY = Math.max(0, centerY - size / 2);
522
+ const sW = Math.min(previewCanvas.width - sX, size);
523
+ const sH = Math.min(previewCanvas.height - sY, size);
524
+
525
+ const faceCanvas = document.createElement('canvas');
526
+ faceCanvas.width = 512;
527
+ faceCanvas.height = 512;
528
+ const fCtx = faceCanvas.getContext('2d');
529
+ fCtx.fillStyle = '#000000';
530
+ fCtx.fillRect(0, 0, 512, 512);
531
+
532
+ const scale = 512 / size;
533
+ const dX = (sX - (centerX - size/2)) * scale;
534
+ const dY = (sY - (centerY - size/2)) * scale;
535
+ const dW = sW * scale;
536
+ const dH = sH * scale;
537
+
538
+ fCtx.drawImage(previewCanvas, sX, sY, sW, sH, dX, dY, dW, dH);
539
+ const faceData = faceCanvas.toDataURL('image/jpeg', 0.95);
540
+ extractedFrames.push({ data: faceData, time: time, type: 'Face' });
541
+ addFrameToUI(faceData, time, 'Face');
542
+ }
543
+ });
544
+ }
545
+
546
+ // 4. Draw visual feedback bounding boxes on monitor
547
+ validEntities.forEach(entity => {
548
+ if (entity.personBox) {
549
+ ctx.strokeStyle = entity.isMatch ? '#c084fc' : '#818cf8';
550
+ ctx.lineWidth = 4;
551
+ ctx.strokeRect(entity.personBox.originX, entity.personBox.originY, entity.personBox.width, entity.personBox.height);
552
+ if (entity.isMatch) {
553
+ ctx.fillStyle = '#c084fc';
554
+ ctx.font = '20px Arial';
555
+ ctx.fillText("TARGET MATCH", entity.personBox.originX, entity.personBox.originY - 10);
556
+ }
557
+ }
558
+ if (entity.faceBox) {
559
+ ctx.strokeStyle = '#34d399'; // Green for face box
560
+ ctx.lineWidth = 2;
561
+ ctx.strokeRect(entity.faceBox.x, entity.faceBox.y, entity.faceBox.width, entity.faceBox.height);
562
+ }
563
+ });
564
+ }
565
+
566
+ // Update Progress
567
+ const pct = Math.min(100, Math.round((time / duration) * 100));
568
+ progressBar.style.width = `${pct}%`;
569
+ progressPercent.innerText = `${pct}%`;
570
+ statsText.innerText = `Extracted ${extractedFrames.length} specific instances.`;
571
+
572
+ const now = performance.now();
573
+ const fps = Math.round(1000 / (now - lastTime));
574
+ document.getElementById('fps-counter').innerText = `${fps} SEEK/S`;
575
+ lastTime = now;
576
+ }
577
+
578
+ cleanup();
579
+ };
580
+ };
581
+
582
+ // Helper to link Face Box with Body Box
583
+ function isFaceInBody(faceBox, bodyBox) {
584
+ if (!bodyBox || !faceBox) return false;
585
+ const fCenterX = faceBox.x + faceBox.width / 2;
586
+ const fCenterY = faceBox.y + faceBox.height / 2;
587
+ return fCenterX >= bodyBox.originX && fCenterX <= bodyBox.originX + bodyBox.width &&
588
+ fCenterY >= bodyBox.originY && fCenterY <= bodyBox.originY + bodyBox.height;
589
+ }
590
+
591
+ function addFrameToUI(src, time, type) {
592
+ const wrapper = document.createElement('div');
593
+ const sizeClasses = type === 'Face' ? "w-32 h-32" : (type === 'Body' ? "h-40 w-auto min-w-[100px]" : "w-64 h-auto aspect-video");
594
+ const badgeColor = type === 'Face' ? 'bg-emerald-600' : (type === 'Body' ? 'bg-indigo-600' : 'bg-slate-600');
595
+
596
+ wrapper.className = `group relative bg-slate-900 rounded-xl overflow-hidden border border-white/5 hover:border-indigo-500/50 transition-all cursor-zoom-in shadow-xl flex-shrink-0 ${sizeClasses}`;
597
+ wrapper.innerHTML = `
598
+ <img src="${src}" class="w-full h-full object-contain bg-black/50">
599
+ <div class="absolute inset-0 bg-indigo-900/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
600
+ <i class="fas fa-search-plus text-white text-xl"></i>
601
+ </div>
602
+ <div class="absolute bottom-2 left-2 bg-black/60 px-2 py-0.5 rounded text-[9px] font-mono text-indigo-300">
603
+ T+ ${time.toFixed(1)}s
604
+ </div>
605
+ <div class="absolute top-2 right-2 ${badgeColor} px-1.5 py-0.5 rounded text-[8px] font-bold text-white uppercase shadow">
606
+ ${type}
607
+ </div>
608
+ `;
609
+
610
+ wrapper.onclick = () => {
611
+ const win = window.open();
612
+ win.document.write(`<img src="${src}" style="max-width:100%; max-height:100vh; display:block; margin:auto; background:#0f172a;">`);
613
+ };
614
+
615
+ resultsEl.appendChild(wrapper);
616
+ const container = document.getElementById('results-container');
617
+ if (container) container.scrollTop = container.scrollHeight;
618
+ }
619
+
620
+ function cleanup() {
621
+ isProcessing = false;
622
+ startBtn.innerHTML = '<i class="fas fa-microchip"></i> Start AI Extraction';
623
+ startBtn.classList.replace('bg-red-600', 'bg-indigo-600');
624
+ if (scannerLine) scannerLine.style.display = "none";
625
+ statusLabel.innerText = "Process Complete";
626
+ if(extractedFrames.length > 0) {
627
+ downloadBtn.classList.remove('hidden');
628
+ } else {
629
+ if (emptyState) emptyState.classList.remove('hidden');
630
+ statsText.innerText = "Scan complete. No matching frames found.";
631
+ }
632
+ }
633
+
634
+ downloadBtn.onclick = async () => {
635
+ const originalText = downloadBtn.innerHTML;
636
+ downloadBtn.innerHTML = '<i class="fas fa-spinner animate-spin"></i> Zipping...';
637
+ downloadBtn.disabled = true;
638
+
639
+ const zip = new JSZip();
640
+ extractedFrames.forEach((f, index) => {
641
+ const base64Data = f.data.replace(/^data:image\/(png|jpg|jpeg);base64,/, "");
642
+ zip.file(`frame_${f.time.toFixed(2)}s_${f.type}_${index}.jpg`, base64Data, {base64: true});
643
+ });
644
+
645
+ try {
646
+ const content = await zip.generateAsync({type: "blob"});
647
+ const url = URL.createObjectURL(content);
648
+ const a = document.createElement('a');
649
+ a.href = url;
650
+ a.download = `HumanFrames_Extracted.zip`;
651
+ a.click();
652
+ } catch (err) {
653
+ console.error("Zipping failed", err);
654
+ } finally {
655
+ downloadBtn.innerHTML = originalText;
656
+ downloadBtn.disabled = false;
657
+ }
658
+ };
659
+ </script>
660
+ </body>
661
+ </html>