usman-khn commited on
Commit
8665c26
·
verified ·
1 Parent(s): d40a474

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +155 -362
app.py CHANGED
@@ -1,30 +1,25 @@
1
  # app.py
2
- # Final production-ready Gradio + embedded React UI for Hugging Face Spaces
3
- # Dark theme + glassmorphism + React-based preview + autoplay frames
4
- # Make sure best_model.pth is uploaded into the same directory.
5
- # Local source path (for tooling): /mnt/data/app.py
6
- SOURCE_APP_PATH = "/mnt/data/app.py"
7
 
8
  import os
 
9
  import torch
10
  import torch.nn as nn
11
  from torchvision import transforms
12
  from PIL import Image
13
- import numpy as np
14
  import gradio as gr
15
- import cv2
16
  import tempfile
17
  import base64
18
- from typing import List, Union
19
 
20
- # ---------------------- MODEL CONFIG ----------------------
21
  SEQUENCE_LENGTH = 16
22
  NUM_CLASSES = 4
23
  MODEL_PATH = "best_model.pth"
24
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
25
  CLASS_NAMES = ["aggressive", "idle", "panic", "normal"]
26
 
27
- # ---------------------- MODEL DEFINITION ----------------------
28
  class CNNLSTM(nn.Module):
29
  def __init__(self, num_classes):
30
  super(CNNLSTM, self).__init__()
@@ -48,401 +43,199 @@ class CNNLSTM(nn.Module):
48
  x, _ = self.lstm(x)
49
  return self.fc(x[:, -1, :])
50
 
51
- # ---------------------- LOAD MODEL ----------------------
52
  def load_model():
53
  if not os.path.exists(MODEL_PATH):
54
- raise FileNotFoundError("Model file missing: best_model.pth in repository root.")
55
- model = CNNLSTM(num_classes=NUM_CLASSES).to(device)
56
  model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
57
  model.eval()
58
  return model
59
 
60
  try:
61
  model = load_model()
62
- except Exception as e:
63
  model = None
64
- print("Model Load Error:", e)
65
 
66
- # ---------------------- TRANSFORMS ----------------------
67
- transform = transforms.Compose([
68
- transforms.Resize((64, 64)),
69
- transforms.ToTensor(),
70
- ])
71
 
72
- # ---------------------- VIDEO FRAME EXTRACTION ----------------------
73
- def extract_frames_from_video(video_path: str, num_frames: int = SEQUENCE_LENGTH):
74
  """
75
- Extract `num_frames` evenly spaced frames from video file path.
76
- Returns list[PIL.Image] or None if not enough frames.
77
  """
78
- frames = []
79
- cap = cv2.VideoCapture(video_path)
80
- if not cap.isOpened():
81
- cap.release()
82
- return None
83
 
84
- total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
85
- if total < num_frames:
86
- cap.release()
87
- return None
 
 
 
 
 
 
 
 
88
 
89
- interval = max(1, total // num_frames)
90
- idx = 0
91
- while len(frames) < num_frames:
92
- cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
93
- ret, frame = cap.read()
94
- if not ret:
95
- break
96
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
97
- frames.append(Image.fromarray(frame))
98
- idx += interval
99
-
100
- cap.release()
101
- if len(frames) < num_frames:
102
  return None
103
- return frames[:num_frames]
104
 
105
- # ---------------------- PREDICTION ----------------------
106
- def predict_from_frames(frames: List[Image.Image]):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  if model is None:
108
- return {"Error": "Model failed to load. Please upload 'best_model.pth' to the repo."}
109
- if len(frames) != SEQUENCE_LENGTH:
110
- return {"Error": f"Need exactly {SEQUENCE_LENGTH} frames (got {len(frames)})."}
111
 
112
- try:
113
- frame_tensors = []
114
- for img in frames:
115
- if img.mode != 'RGB':
116
- img = img.convert('RGB')
117
- frame_tensors.append(transform(img))
118
- video_tensor = torch.stack(frame_tensors).unsqueeze(0).to(device) # (1, T, C, H, W)
119
- with torch.no_grad():
120
- outputs = model(video_tensor)
121
- probs = torch.softmax(outputs, dim=1)[0].cpu().numpy().tolist()
122
- return {CLASS_NAMES[i]: float(probs[i]) for i in range(NUM_CLASSES)}
123
- except Exception as e:
124
- return {"Error": f"Prediction failed: {str(e)}"}
125
-
126
- def predict(input_files: Union[str, List[str]]):
127
- """
128
- Accepts:
129
- - single video filepath string
130
- - single image filepath string
131
- - list of image filepaths (multiple)
132
- Returns label probabilities dict for Gradio Label.
133
- """
134
- # Video path (string) or list of file paths
135
- # Gradio returns a list when file_count="multiple"
136
- files = input_files
137
  if files is None:
138
- return {"Error": "No file provided."}
139
 
140
- # If a single str path (gr.File with single), normalize to list
141
  if isinstance(files, str):
142
  files = [files]
143
 
144
- # If single file and it's a video -> extract frames
145
- if len(files) == 1:
146
- f = files[0]
147
- lower = f.lower()
148
- if lower.endswith(('.mp4', '.mov', '.avi', '.mkv', '.webm')):
149
- frames = extract_frames_from_video(f, SEQUENCE_LENGTH)
150
- if frames is None:
151
- return {"Error": "Video too short or couldn't be read. Needs at least 16 frames."}
152
- return predict_from_frames(frames)
153
- else:
154
- # Treat single image: repeat the same frame to make sequence
155
- try:
156
- img = Image.open(f)
157
- frames = [img.convert("RGB")] * SEQUENCE_LENGTH
158
- return predict_from_frames(frames)
159
- except Exception as e:
160
- return {"Error": f"Could not open image: {e}"}
161
-
162
- # If multiple files: assume images, take first SEQUENCE_LENGTH
163
- if len(files) >= SEQUENCE_LENGTH:
164
- imgs = []
165
- for p in files[:SEQUENCE_LENGTH]:
166
- try:
167
- imgs.append(Image.open(p).convert("RGB"))
168
- except Exception as e:
169
- return {"Error": f"Failed to open one of the images: {e}"}
170
- return predict_from_frames(imgs)
171
- else:
172
- return {"Error": f"Need at least {SEQUENCE_LENGTH} image files (got {len(files)})."}
173
 
174
- # ---------------------- GRADIO UI (Blocks) ----------------------
175
- # We'll embed a small React app inside an HTML block to provide an advanced preview,
176
- # autoplay frames, and glass/dark UI. The React app listens to the file input with id "media_input"
177
- # (we set elem_id for the Gradio file component).
178
 
 
179
  css = """
180
- /* Dark glassmorphism styles */
181
- :root{
182
- --bg:#0b0f12;
183
- --card: rgba(255,255,255,0.04);
184
- --glass: rgba(255,255,255,0.06);
185
- --accent: rgba(59,130,246,0.9);
186
- --muted: rgba(255,255,255,0.6);
187
- }
188
- body, .gradio-container {
189
- background: linear-gradient(180deg, #071018 0%, #0b0f12 100%) !important;
190
- color: #E6EEF3 !important;
191
- }
192
- .gradio-container .block {
193
- background: transparent !important;
194
- }
195
- /* glass card */
196
  .glass {
197
- background: var(--glass);
198
- backdrop-filter: blur(8px) saturate(120%);
199
- -webkit-backdrop-filter: blur(8px) saturate(120%);
200
- border-radius: 14px;
201
- border: 1px solid rgba(255,255,255,0.06);
202
- padding: 18px;
203
- box-shadow: 0 6px 24px rgba(2,6,23,0.6);
204
- }
205
- .title {
206
- font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
207
- font-weight: 700;
208
- font-size: 28px;
209
- letter-spacing: -0.2px;
210
- }
211
- .subtitle {
212
- color: var(--muted);
213
- margin-top: 6px;
214
- margin-bottom: 10px;
215
- }
216
- .controls {
217
- display:flex;
218
- gap:12px;
219
- align-items:center;
220
- }
221
- .preview-area {
222
- display:flex;
223
- gap:12px;
224
- align-items:center;
225
- justify-content:center;
226
- flex-wrap:wrap;
227
- margin-top:12px;
228
- }
229
- #react-root {
230
- width:100%;
231
- }
232
- .frame-thumb {
233
- width: 120px;
234
- height: 80px;
235
- object-fit: cover;
236
- border-radius:8px;
237
- border: 1px solid rgba(255,255,255,0.04);
238
- box-shadow: 0 8px 20px rgba(2,6,23,0.5);
239
- }
240
- .video-preview {
241
- max-width: 420px;
242
- border-radius: 12px;
243
- overflow: hidden;
244
- border: 1px solid rgba(255,255,255,0.04);
245
- }
246
- .info {
247
- color: var(--muted);
248
- font-size: 13px;
249
- }
250
- .btn-ghost {
251
- background: transparent;
252
- border: 1px solid rgba(255,255,255,0.06);
253
- padding: 8px 12px;
254
- border-radius: 10px;
255
- color: var(--muted);
256
- }
257
- .small {
258
- font-size: 13px;
259
- }
260
- .footer {
261
- text-align:center;
262
- color: var(--muted);
263
- font-size:12px;
264
- margin-top:12px;
265
  }
266
  """
267
 
268
- # HTML + React app embed (CDN-based React for simplicity)
269
  react_html = """
270
- <div class="glass" style="padding:16px;">
271
- <div style="display:flex;justify-content:space-between;align-items:center;">
272
- <div>
273
- <div class="title">Crowd Behavior Analyzer</div>
274
- <div class="subtitle">Dark • Glassmorphism • React preview • Autoplay frames</div>
275
- </div>
276
- <div style="text-align:right;">
277
- <div class="info">Model: CNN-LSTM | Frames: 16</div>
278
- </div>
279
- </div>
280
-
281
- <div style="margin-top:12px;">
282
- <div id="react-root"></div>
283
- </div>
284
-
285
- <div class="footer">Upload a video or images using the file picker below. Use "Analyze" to run the model.</div>
286
  </div>
287
 
288
- <!-- React and app script -->
289
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
290
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
 
291
  <script>
292
  const e = React.createElement;
293
 
294
- function PreviewApp(){
295
- const [frames, setFrames] = React.useState([]);
296
- const [isVideo, setIsVideo] = React.useState(false);
297
- const [autoplay, setAutoplay] = React.useState(true);
298
- const [playingIndex, setPlayingIndex] = React.useState(0);
299
- const intervalRef = React.useRef(null);
300
-
301
- // Connect to the Gradio file input by elem_id
302
- React.useEffect(() => {
303
- const input = document.getElementById("media_input");
304
- if(!input) return;
305
-
306
- function handleFiles(event) {
307
- const files = input.files;
308
- if(!files || files.length === 0){
309
- setFrames([]);
310
- return;
311
- }
312
-
313
- // If single file and it's video
314
- if(files.length === 1 && files[0].type.startsWith("video/")){
315
- setIsVideo(true);
316
- const url = URL.createObjectURL(files[0]);
317
- // create a video element, sample frames
318
- const video = document.createElement("video");
319
- video.src = url;
320
- video.crossOrigin = "anonymous";
321
- video.muted = true;
322
- video.playsInline = true;
323
- video.addEventListener('loadedmetadata', async () => {
324
- const duration = video.duration;
325
- const canvas = document.createElement('canvas');
326
- const ctx = canvas.getContext('2d');
327
- canvas.width = 320;
328
- canvas.height = 180;
329
- const count = 16;
330
- const newFrames = [];
331
- for(let i=0;i<count;i++){
332
- const t = Math.min(duration * (i / count), duration - 0.05);
333
- await new Promise((res) => {
334
- video.currentTime = t;
335
- video.addEventListener('seeked', function handler(){
336
- ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
337
- newFrames.push(canvas.toDataURL('image/jpeg', 0.7));
338
- video.removeEventListener('seeked', handler);
339
- res();
340
- });
341
- });
342
- }
343
- setFrames(newFrames);
344
- setPlayingIndex(0);
345
- }, {once:true});
346
- } else {
347
- // treat as images
348
- setIsVideo(false);
349
- const imageFiles = Array.from(files).slice(0,16);
350
- const readers = imageFiles.map(f => {
351
- return new Promise((res, rej) => {
352
- const fr = new FileReader();
353
- fr.onload = () => res(fr.result);
354
- fr.onerror = rej;
355
- fr.readAsDataURL(f);
356
- });
357
  });
358
- Promise.all(readers).then(results => {
359
- const picked = results.slice(0,16);
360
- // If fewer than 16, repeat to make 16 visually
361
- while(picked.length < 16){
362
- picked.push(picked[picked.length % picked.length] || picked[0]);
363
- }
364
- setFrames(picked.slice(0,16));
365
- setPlayingIndex(0);
366
- }).catch(()=> setFrames([]));
367
- }
368
- }
369
-
370
- input.addEventListener('change', handleFiles);
371
- return () => input.removeEventListener('change', handleFiles);
372
- }, []);
373
-
374
- // autoplay logic
375
- React.useEffect(() => {
376
- if(autoplay && frames.length > 0){
377
- intervalRef.current = setInterval(() => {
378
- setPlayingIndex(p => (p+1) % frames.length);
379
- }, 400);
380
- return () => clearInterval(intervalRef.current);
381
- } else {
382
- if(intervalRef.current) clearInterval(intervalRef.current);
383
- }
384
- }, [autoplay, frames]);
385
-
386
- return e('div', {style:{display:'flex', gap:16, flexWrap:'wrap', alignItems:'flex-start'}},
387
- e('div', {style:{flex:'1 1 420px', minWidth:320}},
388
- e('div', {className:"video-preview", style:{padding:12, display:'flex', justifyContent:'center', alignItems:'center', background:'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01))'}},
389
- frames.length > 0 ? e('img', {src: frames[playingIndex], style:{width:'100%', height:'240px', objectFit:'cover', borderRadius:8}}) : e('div', {style:{padding:30, textAlign:'center', color:'rgba(255,255,255,0.6)'}}, "Preview will appear here")
390
- ),
391
- e('div', {style:{display:'flex', justifyContent:'space-between', marginTop:8}},
392
- e('div', {className:'small info'}, frames.length ? `${frames.length} frames prepared` : 'No frames prepared'),
393
- e('div', {},
394
- e('button', {className:'btn-ghost small', onClick: ()=> setAutoplay(a=>!a)}, autoplay? 'Pause' : 'Autoplay')
395
- )
396
- )
397
- ),
398
- e('div', {style:{flex:'0 1 320px', minWidth:260}},
399
- e('div', {style:{display:'grid', gridTemplateColumns:'repeat(2,1fr)', gap:8}},
400
- frames.slice(0,8).map((f,i) => e('img', {key:i, src:f, className:'frame-thumb', onClick: ()=> setPlayingIndex(i)})),
401
- frames.slice(8,16).map((f,i) => e('img', {key:8+i, src:f, className:'frame-thumb', onClick: ()=> setPlayingIndex(8+i)}))
402
- ),
403
- e('div', {style:{marginTop:12, color:'var(--muted)', fontSize:13}},
404
- "Click thumbnails to jump to frame. Drag files to the file picker below to update preview."
405
- )
406
- )
407
  );
408
  }
409
 
410
- const domRoot = document.getElementById('react-root');
411
- if(domRoot) {
412
- ReactDOM.createRoot(domRoot).render(React.createElement(PreviewApp));
413
- }
414
  </script>
415
  """
416
 
417
- # Build the Gradio App
418
- with gr.Blocks(css=css, title="Crowd Behavior Analyzer — Dark Glass UI") as demo:
419
- with gr.Row():
420
- gr.Markdown("<div style='font-size:12px;color:rgba(255,255,255,0.45)'>Source: {}</div>".format(SOURCE_APP_PATH))
421
-
422
- # top glass/react preview
423
- demo.append(gr.HTML(react_html))
424
-
425
- with gr.Row(elem_id="controls_row"):
426
- file_input = gr.File(label="Upload Video (.mp4/.mov/.avi/.mkv) or Images (select multiple)", file_count="multiple", type="filepath", elem_id="media_input")
427
- analyze_btn = gr.Button("Analyze", variant="primary")
428
-
429
- with gr.Row():
430
- result_label = gr.Label(num_top_classes=NUM_CLASSES, label="Prediction (Probabilities)")
431
-
432
- with gr.Row():
433
- notes = gr.Markdown("""
434
- **How to use**
435
- - Upload a single **video**: app will sample 16 frames automatically.
436
- - Upload a single **image**: image will be repeated to form a 16-frame input (quick test).
437
- - Upload **multiple images**: first 16 images will be used.
438
- """)
439
- with gr.Row():
440
- footer = gr.Markdown("<div style='color:rgba(255,255,255,0.45);font-size:12px'>© Crowd Analyzer • Dark Glass UI</div>")
441
-
442
- # Wire up interactions
443
- analyze_btn.click(fn=predict, inputs=file_input, outputs=result_label)
444
-
445
- # Launch
446
- if __name__ == "__main__":
447
- # For Spaces, Gradio will handle host/port automatically.
448
- demo.launch(server_name="0.0.0.0", share=False)
 
1
  # app.py
2
+ # FINAL VERSION No OpenCV. Works on Hugging Face Spaces.
3
+ # Dark theme + Glassmorphism + React autoplay preview
4
+ # Just upload this + best_model.pth
 
 
5
 
6
  import os
7
+ import subprocess
8
  import torch
9
  import torch.nn as nn
10
  from torchvision import transforms
11
  from PIL import Image
 
12
  import gradio as gr
 
13
  import tempfile
14
  import base64
 
15
 
 
16
  SEQUENCE_LENGTH = 16
17
  NUM_CLASSES = 4
18
  MODEL_PATH = "best_model.pth"
19
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
20
  CLASS_NAMES = ["aggressive", "idle", "panic", "normal"]
21
 
22
+ # ------------------ MODEL ------------------
23
  class CNNLSTM(nn.Module):
24
  def __init__(self, num_classes):
25
  super(CNNLSTM, self).__init__()
 
43
  x, _ = self.lstm(x)
44
  return self.fc(x[:, -1, :])
45
 
46
+ # ------------------ LOAD MODEL ------------------
47
  def load_model():
48
  if not os.path.exists(MODEL_PATH):
49
+ raise FileNotFoundError("Upload best_model.pth to the repository.")
50
+ model = CNNLSTM(NUM_CLASSES).to(device)
51
  model.load_state_dict(torch.load(MODEL_PATH, map_location=device))
52
  model.eval()
53
  return model
54
 
55
  try:
56
  model = load_model()
57
+ except:
58
  model = None
 
59
 
 
 
 
 
 
60
 
61
+ # ------------------ FRAME EXTRACTION (FFmpeg) ------------------
62
+ def extract_frames_ffmpeg(video_path):
63
  """
64
+ Extract 16 evenly spaced frames using FFmpeg (preinstalled on Hugging Face Spaces).
65
+ Returns list[PIL.Image].
66
  """
 
 
 
 
 
67
 
68
+ tmp_dir = tempfile.mkdtemp()
69
+
70
+ cmd = [
71
+ "ffmpeg",
72
+ "-i", video_path,
73
+ "-vf", f"fps=1,scale=320:180",
74
+ os.path.join(tmp_dir, "frame_%03d.jpg"),
75
+ "-hide_banner",
76
+ "-loglevel", "error"
77
+ ]
78
+
79
+ subprocess.run(cmd)
80
 
81
+ frames = sorted([os.path.join(tmp_dir, f) for f in os.listdir(tmp_dir) if f.endswith(".jpg")])
82
+
83
+ if len(frames) == 0:
 
 
 
 
 
 
 
 
 
 
84
  return None
 
85
 
86
+ # sample exactly 16 frames evenly
87
+ if len(frames) >= SEQUENCE_LENGTH:
88
+ import numpy as np
89
+ idxs = np.linspace(0, len(frames)-1, SEQUENCE_LENGTH).astype(int)
90
+ frames = [frames[i] for i in idxs]
91
+ else:
92
+ # repeat frames
93
+ frames = (frames * 16)[:16]
94
+
95
+ pil_frames = [Image.open(f).convert("RGB") for f in frames]
96
+ return pil_frames
97
+
98
+
99
+ # ------------------ PREDICTION ------------------
100
+ transform = transforms.Compose([
101
+ transforms.Resize((64, 64)),
102
+ transforms.ToTensor(),
103
+ ])
104
+
105
+ def run_prediction(frames):
106
  if model is None:
107
+ return {"Error": "Model not loaded."}
 
 
108
 
109
+ tensors = [transform(f) for f in frames]
110
+ video_tensor = torch.stack(tensors).unsqueeze(0).to(device)
111
+
112
+ with torch.no_grad():
113
+ out = model(video_tensor)
114
+
115
+ probs = torch.softmax(out, dim=1)[0].cpu().numpy()
116
+
117
+ return {CLASS_NAMES[i]: float(probs[i]) for i in range(NUM_CLASSES)}
118
+
119
+ def predict(files):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  if files is None:
121
+ return {"Error": "Upload a file."}
122
 
123
+ # Normalize file list
124
  if isinstance(files, str):
125
  files = [files]
126
 
127
+ # CASE 1: video
128
+ if len(files) == 1 and files[0].lower().endswith((".mp4",".mov",".avi",".mkv",".webm")):
129
+ frames = extract_frames_ffmpeg(files[0])
130
+ if frames is None:
131
+ return {"Error": "Unable to extract frames from video."}
132
+ return run_prediction(frames)
133
+
134
+ # CASE 2: multiple images
135
+ if len(files) >= 16:
136
+ frames = [Image.open(f).convert("RGB") for f in files[:16]]
137
+ return run_prediction(frames)
138
+
139
+ # CASE 3: single image
140
+ try:
141
+ img = Image.open(files[0]).convert("RGB")
142
+ frames = [img] * 16
143
+ return run_prediction(frames)
144
+ except:
145
+ return {"Error": "Invalid image."}
 
 
 
 
 
 
 
 
 
 
146
 
 
 
 
 
147
 
148
+ # ------------------ UI & React ------------------
149
  css = """
150
+ body, .gradio-container { background: #0b0f12 !important; color: white !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  .glass {
152
+ backdrop-filter: blur(12px) saturate(180%);
153
+ background: rgba(255,255,255,0.06);
154
+ border-radius: 16px;
155
+ padding: 20px;
156
+ border: 1px solid rgba(255,255,255,0.08);
157
+ box-shadow: 0 4px 40px rgba(0,0,0,0.4);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  }
159
  """
160
 
 
161
  react_html = """
162
+ <div class="glass">
163
+ <h1 style="margin:0; font-size:28px;">Crowd Behavior Analyzer</h1>
164
+ <p style="opacity:0.7;">React Preview • Dark • Glassmorphism • Autoplay Frames</p>
165
+ <div id="react-root"></div>
 
 
 
 
 
 
 
 
 
 
 
 
166
  </div>
167
 
 
168
  <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
169
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
170
+
171
  <script>
172
  const e = React.createElement;
173
 
174
+ function App(){
175
+ const [frames,setFrames] = React.useState([]);
176
+ const [index,setIndex] = React.useState(0);
177
+
178
+ React.useEffect(()=>{
179
+ const fileInput = document.getElementById("media_input");
180
+ if(!fileInput) return;
181
+
182
+ const handle = (evt)=>{
183
+ const files = fileInput.files;
184
+ if(!files || files.length === 0) return;
185
+
186
+ // images only for UI preview
187
+ const readers = [...files].slice(0,16).map(file => {
188
+ return new Promise((res)=>{
189
+ const r = new FileReader();
190
+ r.onload = ()=>res(r.result);
191
+ r.readAsDataURL(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  });
193
+ });
194
+
195
+ Promise.all(readers).then(imgs=>{
196
+ if(imgs.length === 0) return;
197
+ while(imgs.length < 16) imgs.push(imgs[0]);
198
+ setFrames(imgs.slice(0,16));
199
+ setIndex(0);
200
+ });
201
+ };
202
+
203
+ fileInput.addEventListener("change",handle);
204
+ return ()=>fileInput.removeEventListener("change",handle);
205
+ },[]);
206
+
207
+ React.useEffect(()=>{
208
+ if(frames.length === 0) return;
209
+ const t = setInterval(()=>setIndex(i=>(i+1)%frames.length),350);
210
+ return ()=>clearInterval(t);
211
+ },[frames]);
212
+
213
+ return e("div",{},
214
+ frames.length
215
+ ? e("img",{src:frames[index], style:{width:"100%",borderRadius:"12px"}})
216
+ : e("p",{style:{opacity:0.6}},"Preview will appear here after upload.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  );
218
  }
219
 
220
+ ReactDOM.createRoot(document.getElementById("react-root")).render(e(App));
 
 
 
221
  </script>
222
  """
223
 
224
+ with gr.Blocks(css=css) as demo:
225
+
226
+ gr.HTML(react_html)
227
+
228
+ file_input = gr.File(
229
+ label="Upload Video or Images",
230
+ file_count="multiple",
231
+ type="filepath",
232
+ elem_id="media_input"
233
+ )
234
+
235
+ btn = gr.Button("Analyze Behavior", variant="primary")
236
+
237
+ output = gr.Label(num_top_classes=4)
238
+
239
+ btn.click(fn=predict, inputs=file_input, outputs=output)
240
+
241
+ demo.launch()