LucaFranceschi commited on
Commit
4aa3f07
·
unverified ·
1 Parent(s): 6302e91

Squashed commit of the following:

Browse files

commit f0f16cb1b4338fe43e07c0d4d8054faec5b743d5
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sun May 3 16:37:59 2026 +0200

Improved shared session state

commit 7659be4fdaa540fdc0b1caadccf93e0136e7331f
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sun May 3 15:10:18 2026 +0200

Works wonderfully

commit 4b919de8d56e97e2d061528acacf1512d0b85156
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sun May 3 11:41:26 2026 +0200

Works mostly. Still todo threshold for video saving

commit c944f58c3fbad73c8216066b3853a1bc7457ca09
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sat May 2 19:57:53 2026 +0200

Checkpoint

commit f5794b8520879dc3de6e65de283c8848856a89aa
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sat May 2 18:51:39 2026 +0200

Checkpoint

commit 4e67442a27e124c9332f56ecf015302328df4423
Author: LucaFranceschi <luca.franceschi01@estudiant.upf.edu>
Date: Sat May 2 12:02:48 2026 +0200

Checkpoint

Files changed (4) hide show
  1. .gitignore +2 -1
  2. Dockerfile +4 -0
  3. app.py +412 -25
  4. utils/viz.py +11 -7
.gitignore CHANGED
@@ -1 +1,2 @@
1
- data
 
 
1
+ data
2
+ .vscode
Dockerfile CHANGED
@@ -7,6 +7,8 @@ ENV PIP_NO_CACHE_DIR=1
7
 
8
  WORKDIR $HOME/app
9
 
 
 
10
  COPY environment.yaml $HOME/app/environment.yaml
11
 
12
  # Run conda/pip as root so it can write to /opt/conda
@@ -24,6 +26,8 @@ RUN useradd -m -u 1000 user
24
 
25
  COPY --exclude=data . $HOME/app
26
  RUN chown -R user:user /home/user/
 
 
27
  USER user
28
 
29
  CMD ["python", "app.py"]
 
7
 
8
  WORKDIR $HOME/app
9
 
10
+ RUN apt-get update && apt-get install -y ffmpeg && apt-get clean && rm -rf /var/lib/apt/lists/*
11
+
12
  COPY environment.yaml $HOME/app/environment.yaml
13
 
14
  # Run conda/pip as root so it can write to /opt/conda
 
26
 
27
  COPY --exclude=data . $HOME/app
28
  RUN chown -R user:user /home/user/
29
+ RUN mkdir -p /tmp/gradio && chown -R user:user /tmp/gradio
30
+
31
  USER user
32
 
33
  CMD ["python", "app.py"]
app.py CHANGED
@@ -1,17 +1,24 @@
1
  import os
2
  import torch
3
  import torchaudio
 
 
 
 
4
 
5
  import numpy as np
6
  import gradio as gr
7
 
8
- from PIL.Image import Image
 
 
9
  from importlib import import_module
10
  from torchvision import transforms as vt
11
 
12
  # from modules.models import ACL, ADCL
13
  from utils.util import get_prompt_template
14
- from utils.viz import draw_overlaid, draw_heatmap
 
15
 
16
  # =========================================== CONSTANTS ===========================================
17
 
@@ -25,6 +32,8 @@ WEIGHTS_SUBPATH = {
25
  'baseline': 'ACL_ViT16_test_best_param/Param_best.pth'
26
  }
27
 
 
 
28
  USE_CUDA = torch.cuda.is_available()
29
 
30
  PROMPT_TEMPLATE, TEXT_POS_AT_PROMPT, PROMPT_LENGTH = get_prompt_template()
@@ -32,17 +41,103 @@ PROMPT_TEMPLATE, TEXT_POS_AT_PROMPT, PROMPT_LENGTH = get_prompt_template()
32
  DEVICE = torch.device('cuda', torch.cuda.current_device()) if USE_CUDA else torch.device('cpu')
33
  print(f'Device: {DEVICE} is used\n')
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # =========================================== FUNCTIONS ===========================================
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  @torch.no_grad()
38
  def forward(
39
  image: torch.Tensor,
40
  audio: torch.Tensor,
41
  model_name: str,
42
  model_version: str,
43
- resolution: tuple[int]
44
- ) -> Image:
45
- # perform a forward pass and return the heatmap as a grayscale image
46
  model = getattr(import_module('modules.models'), model_name)(
47
  CONFIG_FILE_TEMPLATE.format(model_name),
48
  DEVICE,
@@ -54,7 +149,7 @@ def forward(
54
 
55
  placeholder_tokens = model.get_placeholder_token(PROMPT_TEMPLATE.replace('{}', ''))
56
 
57
- min_resolution = min(resolution)
58
 
59
  audio_driven_embedding = model.encode_audio(
60
  audio.to(model.device),
@@ -63,35 +158,36 @@ def forward(
63
  PROMPT_LENGTH
64
  )
65
 
66
- out_dict = model(image.to(DEVICE), resolution=min_resolution, pred_emb=audio_driven_embedding)
67
 
68
- seg = out_dict['positive']
69
 
70
- seg_image = ((seg.squeeze().cpu().numpy()) * 255).astype(np.uint8)
71
 
72
- return draw_heatmap(seg_image, resolution)
73
 
74
  def submit(
75
- image_file: Image,
76
  audio_file: tuple[int, np.ndarray],
77
- # video: UploadFile = File(...),
78
  model_name: str,
79
  model_version: str,
80
- ):
81
- resolution = min(image_file.width, image_file.height)
 
 
 
 
 
82
  image_transform = vt.Compose([
83
  vt.Resize((resolution, resolution), vt.InterpolationMode.BICUBIC),
84
  vt.ToTensor(),
85
  vt.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)), # CLIP
86
  ])
87
- image = image_transform(image_file)
88
 
89
  # simulate batch dimension
90
  image = image.unsqueeze(0)
91
  print(f'{image.shape=}')
92
 
93
- original_resolution = image_file.size
94
-
95
  sr, audio = audio_file
96
  audio = torch.Tensor(audio).T
97
  print(f'{audio.shape=}')
@@ -107,7 +203,222 @@ def submit(
107
  audio = audio.unsqueeze(0)
108
  print(f'{audio.shape=}', f'sample_rate {sr} --> {SAMPLE_RATE}' if sr != SAMPLE_RATE else '')
109
 
110
- return forward(image, audio, model_name, model_version, original_resolution)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  # ========================================== APPLICATION ==========================================
113
 
@@ -123,25 +434,101 @@ choices_versions = {
123
  }
124
  choices_models_init = choices_models[0]
125
 
 
126
  def update_versions(model_name):
127
  return gr.Dropdown(
128
  choices=choices_versions[model_name],
129
  value=choices_versions[model_name][0]
130
  )
131
 
 
132
  with gr.Blocks() as demo:
133
  gr.Markdown("Start typing below and then click **Run** to see the output.")
134
 
 
 
 
 
135
  with gr.Row():
136
- model_name_in = gr.Dropdown(choices=choices_models)
137
- model_version_name_in = gr.Dropdown(choices=choices_versions[choices_models_init])
138
  model_name_in.change(fn=update_versions, inputs=model_name_in, outputs=model_version_name_in)
139
 
140
- audio_in = gr.Audio()
141
- image_in = gr.Image(type='pil')
142
- image_out = gr.Image(type='pil')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- btn = gr.Button("Run")
145
- btn.click(fn=submit, inputs=[image_in, audio_in, model_name_in, model_version_name_in], outputs=image_out)
146
 
147
  demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
 
1
  import os
2
  import torch
3
  import torchaudio
4
+ import cv2
5
+ import subprocess
6
+ import uuid
7
+ import shutil
8
 
9
  import numpy as np
10
  import gradio as gr
11
 
12
+ from typing import cast, TypedDict
13
+ from PIL import Image
14
+ from PIL.Image import Image as PImage
15
  from importlib import import_module
16
  from torchvision import transforms as vt
17
 
18
  # from modules.models import ACL, ADCL
19
  from utils.util import get_prompt_template
20
+ from utils.viz import draw_overlaid, draw_overlaid_im, draw_heatmap
21
+
22
 
23
  # =========================================== CONSTANTS ===========================================
24
 
 
32
  'baseline': 'ACL_ViT16_test_best_param/Param_best.pth'
33
  }
34
 
35
+ MEDIA_DIR = 'media'
36
+
37
  USE_CUDA = torch.cuda.is_available()
38
 
39
  PROMPT_TEMPLATE, TEXT_POS_AT_PROMPT, PROMPT_LENGTH = get_prompt_template()
 
41
  DEVICE = torch.device('cuda', torch.cuda.current_device()) if USE_CUDA else torch.device('cpu')
42
  print(f'Device: {DEVICE} is used\n')
43
 
44
+
45
+ # ======================================= SESSION MANAGEMENT ======================================
46
+
47
+ # required fields. PEP 655 unavailable
48
+ class _SessionState(TypedDict):
49
+ session_id: str
50
+ session_dir: str
51
+
52
+
53
+ class SessionState(_SessionState, total=False):
54
+ """Per-session state dictionary"""
55
+ image_seg: np.ndarray
56
+ image_resolution: tuple[int, int]
57
+ video_seg: np.ndarray
58
+ video_resolution: tuple[int, int]
59
+ video_audio_path: str
60
+ video_fps: int
61
+
62
+
63
+ def create_session() -> SessionState:
64
+ """Create a new session with its own directory"""
65
+ session_id = str(uuid.uuid4())[:8]
66
+ session_dir = os.path.join(MEDIA_DIR, session_id)
67
+ os.makedirs(session_dir, exist_ok=True)
68
+
69
+ print(f'Created session: {session_id} at {session_dir}')
70
+
71
+ return SessionState(
72
+ session_id=session_id,
73
+ session_dir=session_dir
74
+ )
75
+
76
+
77
+ def cleanup_session(state: SessionState) -> None:
78
+ """Clean up session directory and files"""
79
+ if 'session_dir' not in state:
80
+ return
81
+
82
+ session_dir = state['session_dir']
83
+ if os.path.exists(session_dir):
84
+ shutil.rmtree(session_dir)
85
+ print(f'Cleaned up session directory: {session_dir}')
86
+
87
+
88
  # =========================================== FUNCTIONS ===========================================
89
 
90
+ def apply_threshold_to_segmentation(seg: np.ndarray, threshold: float) -> np.ndarray:
91
+ """Apply threshold to segmentation map"""
92
+ seg_thresholded = np.where(seg >= threshold*255, 255, 0).astype(np.uint8)
93
+ return seg_thresholded
94
+
95
+
96
+ def update_threshold(thr: float, state: SessionState) -> PImage:
97
+ """Update threshold for image segmentation"""
98
+ if 'image_seg' not in state or 'image_resolution' not in state:
99
+ return gr.skip() # type: ignore
100
+
101
+ seg_thresholded = apply_threshold_to_segmentation(state['image_seg'], thr)
102
+ heatmap_mask = draw_heatmap(seg_thresholded, state['image_resolution'])
103
+ return Image.fromarray(heatmap_mask)
104
+
105
+
106
+ def update_threshold_video(thr: float, state: SessionState) -> str:
107
+ """Update threshold for video segmentation"""
108
+ if 'video_seg' not in state or \
109
+ 'video_resolution' not in state or \
110
+ 'video_audio_path' not in state or \
111
+ 'video_fps' not in state:
112
+ return gr.skip() # type: ignore
113
+
114
+ seg_thresholded = apply_threshold_to_segmentation(state['video_seg'], thr)
115
+
116
+ v_heatmap_mask = []
117
+ for i in range(seg_thresholded.shape[0]):
118
+ v_seg = seg_thresholded[i]
119
+ v_heatmap_mask.append(draw_heatmap(v_seg, state['video_resolution']))
120
+
121
+ heatmap_mask = save_video(
122
+ v_heatmap_mask,
123
+ state['video_audio_path'],
124
+ os.path.join(state['session_dir'], 'video_mask.mp4'),
125
+ state['video_resolution'],
126
+ state['video_fps']
127
+ )
128
+
129
+ return heatmap_mask
130
+
131
+
132
  @torch.no_grad()
133
  def forward(
134
  image: torch.Tensor,
135
  audio: torch.Tensor,
136
  model_name: str,
137
  model_version: str,
138
+ original_resolution: tuple[int, int]
139
+ ) -> np.ndarray:
140
+ """Perform a forward pass and return the raw segmentation map as numpy array (0-255)"""
141
  model = getattr(import_module('modules.models'), model_name)(
142
  CONFIG_FILE_TEMPLATE.format(model_name),
143
  DEVICE,
 
149
 
150
  placeholder_tokens = model.get_placeholder_token(PROMPT_TEMPLATE.replace('{}', ''))
151
 
152
+ resolution = min(original_resolution)
153
 
154
  audio_driven_embedding = model.encode_audio(
155
  audio.to(model.device),
 
158
  PROMPT_LENGTH
159
  )
160
 
161
+ out_dict = model(image.to(DEVICE), resolution=INPUT_RESOLUTION, pred_emb=audio_driven_embedding)
162
 
163
+ seg = ((out_dict['positive'].squeeze().cpu().numpy()) * 255).astype(np.uint8)
164
 
165
+ return seg
166
 
 
167
 
168
  def submit(
169
+ image_file: PImage,
170
  audio_file: tuple[int, np.ndarray],
 
171
  model_name: str,
172
  model_version: str,
173
+ threshold: float,
174
+ state: SessionState
175
+ ) -> tuple[PImage, PImage, SessionState]:
176
+ """Submit image + audio and return heatmap and overlaid visualization"""
177
+ original_resolution = image_file.size
178
+ resolution = min(original_resolution)
179
+
180
  image_transform = vt.Compose([
181
  vt.Resize((resolution, resolution), vt.InterpolationMode.BICUBIC),
182
  vt.ToTensor(),
183
  vt.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)), # CLIP
184
  ])
185
+ image = cast(torch.Tensor, image_transform(image_file))
186
 
187
  # simulate batch dimension
188
  image = image.unsqueeze(0)
189
  print(f'{image.shape=}')
190
 
 
 
191
  sr, audio = audio_file
192
  audio = torch.Tensor(audio).T
193
  print(f'{audio.shape=}')
 
203
  audio = audio.unsqueeze(0)
204
  print(f'{audio.shape=}', f'sample_rate {sr} --> {SAMPLE_RATE}' if sr != SAMPLE_RATE else '')
205
 
206
+ # Get raw segmentation
207
+ seg = forward(image, audio, model_name, model_version, original_resolution)
208
+
209
+ # Store in state
210
+ state['image_seg'] = seg
211
+ state['image_resolution'] = original_resolution
212
+
213
+ # Create overlaid image
214
+ heatmap = draw_heatmap(seg, original_resolution)
215
+ overlaid = draw_overlaid_im(image_file, Image.fromarray(heatmap))
216
+
217
+ # Apply threshold
218
+ seg_thresholded = apply_threshold_to_segmentation(seg, threshold)
219
+ heatmap_mask = Image.fromarray(draw_heatmap(seg_thresholded, original_resolution))
220
+
221
+ return heatmap_mask, overlaid, state
222
+
223
+
224
+ @torch.no_grad()
225
+ def forward_video(
226
+ frames: torch.Tensor,
227
+ audio: torch.Tensor,
228
+ model_name: str,
229
+ model_version: str,
230
+ original_resolution: tuple[int, int]
231
+ ) -> np.ndarray:
232
+ """Perform forward pass on video frames"""
233
+ model = getattr(import_module('modules.models'), model_name)(
234
+ CONFIG_FILE_TEMPLATE.format(model_name),
235
+ DEVICE,
236
+ MODEL_PATH
237
+ )
238
+
239
+ model.load(os.path.join(WEIGHTS_PATH, WEIGHTS_SUBPATH[model_version]))
240
+ model.train(False)
241
+
242
+ placeholder_tokens = model.get_placeholder_token(PROMPT_TEMPLATE.replace('{}', ''))
243
+
244
+ resolution = min(original_resolution)
245
+
246
+ audio_driven_embedding = model.encode_audio(
247
+ audio.to(model.device),
248
+ placeholder_tokens,
249
+ TEXT_POS_AT_PROMPT,
250
+ PROMPT_LENGTH
251
+ )
252
+
253
+ v_seg = []
254
+ for i in range(frames.shape[0]):
255
+ out_dict = model(
256
+ frames[i].unsqueeze(0).to(DEVICE),
257
+ resolution=INPUT_RESOLUTION,
258
+ pred_emb=audio_driven_embedding
259
+ )
260
+
261
+ seg = ((out_dict['positive'].squeeze().cpu().numpy()) * 255).astype(np.uint8)
262
+
263
+ v_seg.append(seg)
264
+
265
+ return np.array(v_seg)
266
+
267
+
268
+ def save_video(
269
+ video_frames: list[np.ndarray],
270
+ audio_path: str,
271
+ output_path: str,
272
+ original_resolution: tuple[int, int],
273
+ fps: int
274
+ ) -> str:
275
+ """Save video frames with audio to file"""
276
+ # Write to temp AVI with OpenCV
277
+ temp_video = os.path.join(os.path.dirname(output_path), str(uuid.uuid4()) + '.avi')
278
+
279
+ video = cv2.VideoWriter(
280
+ temp_video,
281
+ cv2.VideoWriter.fourcc(*'MJPG'),
282
+ fps,
283
+ original_resolution
284
+ )
285
+
286
+ for i in range(len(video_frames)):
287
+ frame = video_frames[i]
288
+
289
+ if len(frame.shape) == 2: # Grayscale (H, W)
290
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
291
+ elif frame.shape[2] == 3: # RGB image (H, W, 3)
292
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
293
+ else: # Already BGR or other format
294
+ frame_bgr = frame
295
+
296
+ frame_bgr = cv2.flip(frame_bgr, 1)
297
+
298
+ video.write(frame_bgr)
299
+
300
+ video.release()
301
+
302
+ # Convert to browser-compatible MP4 using ffmpeg
303
+ subprocess.run(
304
+ [
305
+ 'ffmpeg', '-y', '-i', temp_video, '-i', audio_path, '-c:v', 'libx264', '-preset', 'fast',
306
+ '-crf', '23', '-c:a', 'aac', '-map', '0:v:0', '-map', '1:a:0', output_path
307
+ ],
308
+ capture_output=True,
309
+ check=True
310
+ )
311
+
312
+ os.remove(temp_video)
313
+
314
+ return output_path
315
+
316
+
317
+ def submit_video(
318
+ video_file: str,
319
+ model_name: str,
320
+ model_version: str,
321
+ threshold: float,
322
+ state: SessionState
323
+ ) -> tuple[str, str, SessionState]:
324
+ """Submit video and return heatmap and overlaid visualization"""
325
+ # Extract video frames
326
+ video = cv2.VideoCapture(video_file)
327
+ original_resolution = (
328
+ int(video.get(cv2.CAP_PROP_FRAME_WIDTH)),
329
+ int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
330
+ )
331
+
332
+ fps = int(video.get(cv2.CAP_PROP_FPS))
333
+
334
+ resolution = min(original_resolution)
335
+ image_transform = vt.Compose([
336
+ vt.Resize((resolution, resolution), vt.InterpolationMode.BICUBIC),
337
+ vt.ToTensor(),
338
+ vt.Normalize((0.48145466, 0.4578275, 0.40821073), (0.26862954, 0.26130258, 0.27577711)), # CLIP
339
+ ])
340
+
341
+ original_frames = []
342
+ frames = []
343
+ while True:
344
+ ret, frame = video.read()
345
+ if not ret:
346
+ break
347
+ frame = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
348
+ original_frames.append(frame)
349
+ frames.append(cast(torch.Tensor, image_transform(frame)))
350
+ video.release()
351
+
352
+ frames = torch.stack(frames)
353
+
354
+ # Extract audio using ffmpeg subprocess
355
+ audio_path = os.path.join(state['session_dir'], 'extracted_audio.wav')
356
+
357
+ try:
358
+ subprocess.run(
359
+ [
360
+ 'ffmpeg', '-i', video_file, '-vn', '-acodec', 'pcm_s16le', '-ar', '16000',
361
+ '-ac', '1', '-y', audio_path
362
+ ],
363
+ capture_output=True,
364
+ check=True
365
+ )
366
+ except subprocess.CalledProcessError as e:
367
+ print(f"FFmpeg error: {e.stderr.decode()}")
368
+ raise
369
+
370
+ # Load extracted audio
371
+ audio, sr = torchaudio.load(audio_path) # type: ignore
372
+
373
+ # Resample if needed
374
+ if sr != SAMPLE_RATE:
375
+ resampler = torchaudio.transforms.Resample(sr, SAMPLE_RATE)
376
+ audio = resampler(audio)
377
+
378
+ # Convert to mono if stereo
379
+ if audio.shape[0] > 1:
380
+ audio = audio.mean(dim=0)
381
+
382
+ print(f'{audio.shape=}')
383
+
384
+ v_seg = forward_video(frames, audio, model_name, model_version, original_resolution)
385
+
386
+ # Store in state
387
+ state['video_seg'] = v_seg
388
+ state['video_resolution'] = original_resolution
389
+ state['video_audio_path'] = audio_path
390
+ state['video_fps'] = fps
391
+
392
+ # Create overlaid image
393
+ v_overlaid = []
394
+ v_heatmap_mask = []
395
+ for i in range(v_seg.shape[0]):
396
+ seg = v_seg[i]
397
+ heatmap = draw_heatmap(seg, original_resolution)
398
+ v_overlaid.append(draw_overlaid(np.array(original_frames[i]), heatmap))
399
+
400
+ # Apply threshold
401
+ seg_thresholded = apply_threshold_to_segmentation(seg, threshold)
402
+ v_heatmap_mask.append(draw_heatmap(seg_thresholded, original_resolution))
403
+
404
+ overlaid = save_video(
405
+ v_overlaid,
406
+ audio_path,
407
+ os.path.join(state['session_dir'], 'video_overlaid.mp4'),
408
+ original_resolution,
409
+ fps
410
+ )
411
+
412
+ heatmap_mask = save_video(
413
+ v_heatmap_mask,
414
+ audio_path,
415
+ os.path.join(state['session_dir'], 'video_mask.mp4'),
416
+ original_resolution,
417
+ fps
418
+ )
419
+
420
+ return heatmap_mask, overlaid, state
421
+
422
 
423
  # ========================================== APPLICATION ==========================================
424
 
 
434
  }
435
  choices_models_init = choices_models[0]
436
 
437
+
438
  def update_versions(model_name):
439
  return gr.Dropdown(
440
  choices=choices_versions[model_name],
441
  value=choices_versions[model_name][0]
442
  )
443
 
444
+
445
  with gr.Blocks() as demo:
446
  gr.Markdown("Start typing below and then click **Run** to see the output.")
447
 
448
+ # Initialize session state per client
449
+ session_state = gr.State(delete_callback=cleanup_session)
450
+ demo.load(fn=create_session, outputs=session_state)
451
+
452
  with gr.Row():
453
+ model_name_in = gr.Dropdown(choices=choices_models, label="Model")
454
+ model_version_name_in = gr.Dropdown(choices=choices_versions[choices_models_init], label="Version")
455
  model_name_in.change(fn=update_versions, inputs=model_name_in, outputs=model_version_name_in)
456
 
457
+ with gr.Tabs():
458
+ # ============= IMAGE + AUDIO TAB =============
459
+ with gr.TabItem("Image + Audio"):
460
+ with gr.Row():
461
+ image_in = gr.Image(type='pil', label="Image Input")
462
+ audio_in = gr.Audio(label="Audio Input")
463
+
464
+ btn = gr.Button("Run")
465
+
466
+ with gr.Row():
467
+ heatmap_out = gr.Image(type='pil', label="Heatmap (Grayscale)")
468
+ overlaid_out = gr.Image(type='pil', label="Overlaid with Original")
469
+
470
+ with gr.Row():
471
+ threshold_slider = gr.Slider(
472
+ minimum=0,
473
+ maximum=1,
474
+ value=0.5,
475
+ step=0.01,
476
+ label="Threshold",
477
+ info="Lower = more sensitive, Higher = less sensitive",
478
+ interactive=False
479
+ )
480
+
481
+ btn.click(
482
+ fn=submit,
483
+ inputs=[image_in, audio_in, model_name_in, model_version_name_in, threshold_slider, session_state],
484
+ outputs=[heatmap_out, overlaid_out, session_state]
485
+ ).then(
486
+ fn=lambda: gr.update(interactive=True), # Enable slider after results
487
+ outputs=threshold_slider
488
+ )
489
+
490
+ threshold_slider.change(
491
+ fn=update_threshold,
492
+ inputs=[threshold_slider, session_state],
493
+ outputs=heatmap_out
494
+ )
495
+
496
+ # ============= VIDEO TAB =============
497
+ with gr.TabItem("Video"):
498
+ with gr.Row():
499
+ video_in = gr.Video(label="Video Input")
500
+
501
+ btn_video = gr.Button("Run")
502
+
503
+ with gr.Row():
504
+ v_heatmap_out = gr.Video(label="Heatmap (Grayscale)")
505
+ v_overlaid_out = gr.Video(label="Overlaid with Original")
506
+
507
+ with gr.Row():
508
+ threshold_slider_video = gr.Slider(
509
+ minimum=0,
510
+ maximum=1,
511
+ value=0.5,
512
+ step=0.01,
513
+ label="Threshold",
514
+ info="Lower = more sensitive, Higher = less sensitive",
515
+ interactive=False
516
+ )
517
+
518
+ btn_video.click(
519
+ fn=submit_video,
520
+ inputs=[video_in, model_name_in, model_version_name_in, threshold_slider_video, session_state],
521
+ outputs=[v_heatmap_out, v_overlaid_out, session_state]
522
+ ).then(
523
+ fn=lambda: gr.update(interactive=True), # Enable slider after results
524
+ outputs=threshold_slider_video
525
+ )
526
+
527
+ threshold_slider_video.change(
528
+ fn=update_threshold_video,
529
+ inputs=[threshold_slider_video, session_state],
530
+ outputs=v_heatmap_out
531
+ )
532
 
 
 
533
 
534
  demo.launch(server_name="0.0.0.0", server_port=7860, debug=True)
utils/viz.py CHANGED
@@ -1,15 +1,19 @@
1
  import cv2, torch
2
  from PIL import Image
 
3
  import numpy as np
4
  from torchvision import transforms as vt
5
 
6
- def draw_overlaid(original_image: Image.Image, heatmap_image: Image.Image) -> Image:
7
- heatmap_array = cv2.applyColorMap(np.array(heatmap_image), cv2.COLORMAP_JET)
8
  heatmap_array = cv2.cvtColor(heatmap_array, cv2.COLOR_BGR2RGB)
9
- overlaid_array = cv2.addWeighted(np.array(original_image), 0.5, heatmap_array, 0.5, 0)
 
 
 
10
  return Image.fromarray(overlaid_array)
11
 
12
- def draw_heatmap(heatmap_image: np.array, resolution: tuple[int]) -> Image:
13
- heatmap_image = Image.fromarray(heatmap_image, 'L')
14
- heatmap_image = heatmap_image.resize(resolution, Image.BICUBIC)
15
- return heatmap_image
 
1
  import cv2, torch
2
  from PIL import Image
3
+ from PIL.Image import Image as PImage
4
  import numpy as np
5
  from torchvision import transforms as vt
6
 
7
+ def draw_overlaid(original_image: np.ndarray, heatmap_image: np.ndarray) -> np.ndarray:
8
+ heatmap_array = cv2.applyColorMap(heatmap_image, cv2.COLORMAP_JET)
9
  heatmap_array = cv2.cvtColor(heatmap_array, cv2.COLOR_BGR2RGB)
10
+ return cv2.addWeighted(original_image, 0.5, heatmap_array, 0.5, 0)
11
+
12
+ def draw_overlaid_im(original_image: PImage, heatmap_image: PImage) -> PImage:
13
+ overlaid_array = draw_overlaid(np.array(original_image), np.array(heatmap_image))
14
  return Image.fromarray(overlaid_array)
15
 
16
+ def draw_heatmap(heatmap_image: np.ndarray, resolution: tuple[int, int]) -> np.ndarray:
17
+ heatmap_result = Image.fromarray(heatmap_image, 'L')
18
+ heatmap_result = heatmap_result.resize(resolution, Image.Resampling.BICUBIC)
19
+ return np.array(heatmap_result)