prithivMLmods commited on
Commit
d2ba389
Β·
verified Β·
1 Parent(s): 378321d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +123 -37
app.py CHANGED
@@ -5,6 +5,7 @@ import numpy as np
5
  import os
6
  import tempfile
7
  import spaces
 
8
  from typing import Optional
9
 
10
  # ---------------------------------------------------------
@@ -17,7 +18,6 @@ try:
17
  from sam3.model_builder import build_sam3_video_predictor
18
  except ImportError as e:
19
  print(f"Warning: Essential libraries (sam_audio, sam3, torchcodec) not found. {e}")
20
- # Define dummy placeholders to prevent immediate crash if libs are missing during check
21
  SAMAudio = None
22
  SAMAudioProcessor = None
23
  build_sam3_video_predictor = None
@@ -54,8 +54,6 @@ except Exception as e:
54
  print("Loading SAM3 Video Predictor...")
55
  try:
56
  video_predictor = build_sam3_video_predictor()
57
- # If SAM3 needs specific device movement, it's usually handled internally or via .to(device)
58
- # dependent on implementation, but standard build function usually sets it up.
59
  print("βœ… SAM3 loaded successfully.")
60
  except Exception as e:
61
  print(f"❌ Error loading SAM3: {e}")
@@ -157,9 +155,8 @@ def save_audio_temp(tensor, sample_rate):
157
  # ---------------------------------------------------------
158
  # Tab 1: Text Prompting (with Chunking)
159
  # ---------------------------------------------------------
160
- @spaces.GPU(duration=120)
161
  def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_DURATION, progress=gr.Progress()):
162
- # Global models are already loaded
163
  global model, processor
164
 
165
  progress(0.05, desc="Checking inputs...")
@@ -192,17 +189,14 @@ def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_
192
  chunk_progress = 0.2 + (i / num_chunks) * 0.6
193
  progress(chunk_progress, desc=f"Processing chunk {i+1}/{num_chunks}...")
194
 
195
- # Save chunk to temp file for processor
196
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
197
  torchaudio.save(tmp.name, chunk, sample_rate)
198
  chunk_path = tmp.name
199
 
200
  try:
201
  inputs = processor(audios=[chunk_path], descriptions=[text_prompt.strip()]).to(device)
202
-
203
  with torch.inference_mode():
204
  result = model.separate(inputs, predict_spans=False, reranking_candidates=1)
205
-
206
  target_chunks.append(result.target[0].cpu())
207
  residual_chunks.append(result.residual[0].cpu())
208
  finally:
@@ -217,10 +211,9 @@ def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_
217
  target_path = save_audio_temp(target_merged, sample_rate)
218
  residual_path = save_audio_temp(residual_merged, sample_rate)
219
 
220
- progress(1.0, desc="Done!")
221
- return target_path, residual_path, f"βœ… Isolated '{text_prompt}' ({num_chunks} chunks processed)"
222
  else:
223
- # Process without chunking (faster for short files)
224
  progress(0.3, desc="Processing audio...")
225
  inputs = processor(audios=[file_path], descriptions=[text_prompt.strip()]).to(device)
226
 
@@ -233,7 +226,6 @@ def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_
233
  target_path = save_audio_temp(result.target[0].unsqueeze(0).cpu(), sr)
234
  residual_path = save_audio_temp(result.residual[0].unsqueeze(0).cpu(), sr)
235
 
236
- progress(1.0, desc="Done!")
237
  return target_path, residual_path, f"βœ… Isolated '{text_prompt}'"
238
 
239
  except Exception as e:
@@ -244,9 +236,8 @@ def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_
244
  # ---------------------------------------------------------
245
  # Tab 2: Visual Prompting
246
  # ---------------------------------------------------------
247
- @spaces.GPU(duration=180)
248
  def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progress()):
249
- # Global models are already loaded
250
  global model, processor, video_predictor
251
 
252
  if model is None or video_predictor is None:
@@ -257,14 +248,10 @@ def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progres
257
 
258
  try:
259
  progress(0.1, desc="Initializing Video Decoder...")
260
- # 1. Initialize Video Decoder
261
  decoder = VideoDecoder(video_file)
262
- frames = decoder[:] # Get all frames (careful with memory on long videos)
263
 
264
- # 2. Generate Masks using SAM3
265
  progress(0.2, desc="Starting SAM3 Session...")
266
-
267
- # Start SAM3 Session
268
  response = video_predictor.handle_request({
269
  "type": "start_session",
270
  "resource_path": video_file,
@@ -273,9 +260,7 @@ def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progres
273
 
274
  progress(0.3, desc=f"Generating masks for {len(decoder)} frames...")
275
  masks = []
276
-
277
- # Frame-by-frame masking
278
- step = 1 # Process every frame
279
  total_frames = len(decoder)
280
 
281
  for frame_index in range(0, total_frames, step):
@@ -292,16 +277,14 @@ def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progres
292
 
293
  if mask.shape[0] == 0:
294
  mask = np.zeros_like(frames[0, [0]], dtype=bool)
295
- masks.append(mask[:1]) # Take first mask
296
 
297
- # Concatenate masks: (Time, 1, H, W)
298
  final_mask = torch.from_numpy(np.concatenate(masks)).unsqueeze(1)
299
 
300
  progress(0.8, desc="Separating Audio with SAM-Audio...")
301
- # 3. Process with SAM-Audio using the visual mask
302
  inputs = processor(
303
  audios=[video_file],
304
- descriptions=[""], # Empty description as we use masks
305
  masked_videos=processor.mask_videos([frames], [final_mask]),
306
  ).to(device)
307
 
@@ -320,35 +303,108 @@ def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progres
320
  traceback.print_exc()
321
  return None, None, f"❌ Error: {str(e)}"
322
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
  # ---------------------------------------------------------
324
  # Gradio Interface
325
  # ---------------------------------------------------------
326
  css = """
327
  #main-title h1 {font-size: 2.3em !important; text-align: center;}
328
- .gradio-container {max-width: 1400px !important; margin: auto;}
329
  """
330
 
331
  with gr.Blocks(css=css) as demo:
332
- gr.Markdown("# **SAM-Audio-Demo**", elem_id="main-title")
333
- gr.Markdown("Segment and isolate sounds using **Text** or **Visual** prompts.")
334
 
335
  with gr.Tabs():
336
  # ================= TAB 1: TEXT =================
337
  with gr.Tab("Text Prompting"):
338
  gr.Markdown("### Isolate sound using a text description")
339
- gr.Markdown("Supports long audio files via auto-chunking.")
340
 
341
  with gr.Row():
342
  with gr.Column():
343
  t1_input = gr.Audio(label="Input Audio/Video", type="filepath")
344
  t1_desc = gr.Textbox(label="Description", placeholder="e.g., 'A man speaking', 'Glass breaking'")
345
- t1_chunk = gr.Slider(minimum=10, maximum=60, value=30, step=5, label="Chunk Duration (seconds)", info="Processing window size. Smaller is faster but loses context.")
346
  t1_btn = gr.Button("Separate Audio", variant="primary")
347
 
348
  with gr.Column():
349
  t1_status = gr.Textbox(label="Status", interactive=False)
350
- t1_target = gr.Audio(label="Target Audio (Isolated)", type="filepath")
351
- t1_residual = gr.Audio(label="Residual Audio (Background)", type="filepath")
352
 
353
  t1_btn.click(
354
  fn=process_text_prompting,
@@ -358,18 +414,17 @@ with gr.Blocks(css=css) as demo:
358
 
359
  # ================= TAB 2: VISUAL =================
360
  with gr.Tab("Visual Prompting"):
361
- gr.Markdown("### Isolate sound corresponding to a visual object in a video")
362
- gr.Markdown("*Note: Uses SAM3 for object detection. Processing is heavier.*")
363
 
364
  with gr.Row():
365
  with gr.Column():
366
  t2_input = gr.Video(label="Input Video", format="mp4")
367
- t2_visual_desc = gr.Textbox(label="Visual Object Description", placeholder="e.g., 'The person on the left', 'The red car'")
368
  t2_btn = gr.Button("Generate Mask & Separate", variant="primary")
369
 
370
  with gr.Column():
371
  t2_status = gr.Textbox(label="Status", interactive=False)
372
- t2_target = gr.Audio(label="Target Audio (From Object)", type="filepath")
373
  t2_residual = gr.Audio(label="Residual Audio", type="filepath")
374
 
375
  t2_btn.click(
@@ -378,6 +433,37 @@ with gr.Blocks(css=css) as demo:
378
  outputs=[t2_target, t2_residual, t2_status]
379
  )
380
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  if __name__ == "__main__":
382
  demo.launch(theme=gr.themes.Soft(
383
  primary_hue="blue",
 
5
  import os
6
  import tempfile
7
  import spaces
8
+ import pandas as pd
9
  from typing import Optional
10
 
11
  # ---------------------------------------------------------
 
18
  from sam3.model_builder import build_sam3_video_predictor
19
  except ImportError as e:
20
  print(f"Warning: Essential libraries (sam_audio, sam3, torchcodec) not found. {e}")
 
21
  SAMAudio = None
22
  SAMAudioProcessor = None
23
  build_sam3_video_predictor = None
 
54
  print("Loading SAM3 Video Predictor...")
55
  try:
56
  video_predictor = build_sam3_video_predictor()
 
 
57
  print("βœ… SAM3 loaded successfully.")
58
  except Exception as e:
59
  print(f"❌ Error loading SAM3: {e}")
 
155
  # ---------------------------------------------------------
156
  # Tab 1: Text Prompting (with Chunking)
157
  # ---------------------------------------------------------
158
+ @spaces.GPU(duration=300)
159
  def process_text_prompting(file_path, text_prompt, chunk_duration=DEFAULT_CHUNK_DURATION, progress=gr.Progress()):
 
160
  global model, processor
161
 
162
  progress(0.05, desc="Checking inputs...")
 
189
  chunk_progress = 0.2 + (i / num_chunks) * 0.6
190
  progress(chunk_progress, desc=f"Processing chunk {i+1}/{num_chunks}...")
191
 
 
192
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
193
  torchaudio.save(tmp.name, chunk, sample_rate)
194
  chunk_path = tmp.name
195
 
196
  try:
197
  inputs = processor(audios=[chunk_path], descriptions=[text_prompt.strip()]).to(device)
 
198
  with torch.inference_mode():
199
  result = model.separate(inputs, predict_spans=False, reranking_candidates=1)
 
200
  target_chunks.append(result.target[0].cpu())
201
  residual_chunks.append(result.residual[0].cpu())
202
  finally:
 
211
  target_path = save_audio_temp(target_merged, sample_rate)
212
  residual_path = save_audio_temp(residual_merged, sample_rate)
213
 
214
+ return target_path, residual_path, f"βœ… Isolated '{text_prompt}' ({num_chunks} chunks)"
 
215
  else:
216
+ # Process without chunking
217
  progress(0.3, desc="Processing audio...")
218
  inputs = processor(audios=[file_path], descriptions=[text_prompt.strip()]).to(device)
219
 
 
226
  target_path = save_audio_temp(result.target[0].unsqueeze(0).cpu(), sr)
227
  residual_path = save_audio_temp(result.residual[0].unsqueeze(0).cpu(), sr)
228
 
 
229
  return target_path, residual_path, f"βœ… Isolated '{text_prompt}'"
230
 
231
  except Exception as e:
 
236
  # ---------------------------------------------------------
237
  # Tab 2: Visual Prompting
238
  # ---------------------------------------------------------
239
+ @spaces.GPU
240
  def process_visual_prompting(video_file, visual_prompt_text, progress=gr.Progress()):
 
241
  global model, processor, video_predictor
242
 
243
  if model is None or video_predictor is None:
 
248
 
249
  try:
250
  progress(0.1, desc="Initializing Video Decoder...")
 
251
  decoder = VideoDecoder(video_file)
252
+ frames = decoder[:]
253
 
 
254
  progress(0.2, desc="Starting SAM3 Session...")
 
 
255
  response = video_predictor.handle_request({
256
  "type": "start_session",
257
  "resource_path": video_file,
 
260
 
261
  progress(0.3, desc=f"Generating masks for {len(decoder)} frames...")
262
  masks = []
263
+ step = 1
 
 
264
  total_frames = len(decoder)
265
 
266
  for frame_index in range(0, total_frames, step):
 
277
 
278
  if mask.shape[0] == 0:
279
  mask = np.zeros_like(frames[0, [0]], dtype=bool)
280
+ masks.append(mask[:1])
281
 
 
282
  final_mask = torch.from_numpy(np.concatenate(masks)).unsqueeze(1)
283
 
284
  progress(0.8, desc="Separating Audio with SAM-Audio...")
 
285
  inputs = processor(
286
  audios=[video_file],
287
+ descriptions=[""],
288
  masked_videos=processor.mask_videos([frames], [final_mask]),
289
  ).to(device)
290
 
 
303
  traceback.print_exc()
304
  return None, None, f"❌ Error: {str(e)}"
305
 
306
+ # ---------------------------------------------------------
307
+ # Tab 3: Span Prompting (Temporal Anchors)
308
+ # ---------------------------------------------------------
309
+ @spaces.GPU
310
+ def process_span_prompting(audio_file, description, anchors_df, progress=gr.Progress()):
311
+ global model, processor
312
+
313
+ if model is None:
314
+ return None, None, "❌ Model not loaded."
315
+ if audio_file is None:
316
+ return None, None, "❌ Please upload an audio file."
317
+
318
+ try:
319
+ progress(0.1, desc="Parsing anchors...")
320
+ # Parse Dataframe to List: [["+", 6.3, 7.0], ...]
321
+ formatted_anchors = []
322
+
323
+ # Validating anchors_df (it usually comes as a list of lists from Gradio Dataframe)
324
+ if anchors_df is not None:
325
+ # Handle standard list of lists
326
+ if isinstance(anchors_df, list):
327
+ iterable = anchors_df
328
+ # Handle pandas DataFrame if Gradio returns that (depends on config)
329
+ elif hasattr(anchors_df, "values"):
330
+ iterable = anchors_df.values.tolist()
331
+ else:
332
+ iterable = []
333
+
334
+ for row in iterable:
335
+ # row structure: [Type, Start, End]
336
+ # Ensure we have data
337
+ if len(row) >= 3 and row[0] and row[1] is not None and row[2] is not None:
338
+ # Validate Type
339
+ anc_type = str(row[0]).strip()
340
+ if anc_type not in ["+", "-"]:
341
+ continue # Skip invalid rows
342
+
343
+ try:
344
+ start_t = float(row[1])
345
+ end_t = float(row[2])
346
+ formatted_anchors.append([anc_type, start_t, end_t])
347
+ except ValueError:
348
+ continue # Skip if numbers aren't valid
349
+
350
+ if not formatted_anchors:
351
+ return None, None, "❌ No valid anchors provided. Use '+' or '-' and valid time numbers."
352
+
353
+ print(f"Anchors: {formatted_anchors}")
354
+
355
+ progress(0.3, desc="Processing with Anchors...")
356
+
357
+ # Note: We do NOT use chunking here because anchors rely on absolute timestamps
358
+ # of the original file.
359
+ inputs = processor(
360
+ audios=[audio_file],
361
+ descriptions=[description if description else "Sound"], # Desc can be generic if anchors are strong
362
+ anchors=[formatted_anchors],
363
+ ).to(device)
364
+
365
+ with torch.inference_mode():
366
+ result = model.separate(inputs)
367
+
368
+ progress(0.9, desc="Saving results...")
369
+ sr = processor.audio_sampling_rate
370
+ target_path = save_audio_temp(result.target[0].unsqueeze(0).cpu(), sr)
371
+ residual_path = save_audio_temp(result.residual[0].unsqueeze(0).cpu(), sr)
372
+
373
+ return target_path, residual_path, f"βœ… Processed with {len(formatted_anchors)} anchors."
374
+
375
+ except Exception as e:
376
+ import traceback
377
+ traceback.print_exc()
378
+ return None, None, f"❌ Error: {str(e)}"
379
+
380
  # ---------------------------------------------------------
381
  # Gradio Interface
382
  # ---------------------------------------------------------
383
  css = """
384
  #main-title h1 {font-size: 2.3em !important; text-align: center;}
385
+ .gradio-container {max-width: 1000px !important; margin: auto;}
386
  """
387
 
388
  with gr.Blocks(css=css) as demo:
389
+ gr.Markdown("# **SAM-Audio** πŸ”Š", elem_id="main-title")
390
+ gr.Markdown("Segment and isolate sounds using **Text**, **Visual**, or **Time-based** prompts.")
391
 
392
  with gr.Tabs():
393
  # ================= TAB 1: TEXT =================
394
  with gr.Tab("Text Prompting"):
395
  gr.Markdown("### Isolate sound using a text description")
 
396
 
397
  with gr.Row():
398
  with gr.Column():
399
  t1_input = gr.Audio(label="Input Audio/Video", type="filepath")
400
  t1_desc = gr.Textbox(label="Description", placeholder="e.g., 'A man speaking', 'Glass breaking'")
401
+ t1_chunk = gr.Slider(minimum=10, maximum=60, value=30, step=5, label="Chunk Duration (s)", info="Split long audio into chunks.")
402
  t1_btn = gr.Button("Separate Audio", variant="primary")
403
 
404
  with gr.Column():
405
  t1_status = gr.Textbox(label="Status", interactive=False)
406
+ t1_target = gr.Audio(label="Target Audio", type="filepath")
407
+ t1_residual = gr.Audio(label="Residual Audio", type="filepath")
408
 
409
  t1_btn.click(
410
  fn=process_text_prompting,
 
414
 
415
  # ================= TAB 2: VISUAL =================
416
  with gr.Tab("Visual Prompting"):
417
+ gr.Markdown("### Isolate sound corresponding to a visual object")
 
418
 
419
  with gr.Row():
420
  with gr.Column():
421
  t2_input = gr.Video(label="Input Video", format="mp4")
422
+ t2_visual_desc = gr.Textbox(label="Visual Object Description", placeholder="e.g., 'The person on the left'")
423
  t2_btn = gr.Button("Generate Mask & Separate", variant="primary")
424
 
425
  with gr.Column():
426
  t2_status = gr.Textbox(label="Status", interactive=False)
427
+ t2_target = gr.Audio(label="Target Audio", type="filepath")
428
  t2_residual = gr.Audio(label="Residual Audio", type="filepath")
429
 
430
  t2_btn.click(
 
433
  outputs=[t2_target, t2_residual, t2_status]
434
  )
435
 
436
+ # ================= TAB 3: SPAN =================
437
+ with gr.Tab("Span Prompting"):
438
+ gr.Markdown("### Isolate sound using Temporal Anchors (Time Ranges)")
439
+ gr.Markdown("Specify when a sound **IS (+)** or **IS NOT (-)** present.")
440
+
441
+ with gr.Row():
442
+ with gr.Column():
443
+ t3_input = gr.Audio(label="Input Audio", type="filepath")
444
+ t3_desc = gr.Textbox(label="Description (Optional)", placeholder="e.g. 'A horn honking'")
445
+
446
+ t3_anchors = gr.Dataframe(
447
+ headers=["Type (+/-)", "Start (s)", "End (s)"],
448
+ datatype=["str", "number", "number"],
449
+ row_count=3,
450
+ col_count=(3, "fixed"),
451
+ label="Temporal Anchors",
452
+ value=[["+", 0.0, 5.0], ["-", 5.0, 10.0], ["", None, None]]
453
+ )
454
+ t3_btn = gr.Button("Separate with Anchors", variant="primary")
455
+
456
+ with gr.Column():
457
+ t3_status = gr.Textbox(label="Status", interactive=False)
458
+ t3_target = gr.Audio(label="Target Audio", type="filepath")
459
+ t3_residual = gr.Audio(label="Residual Audio", type="filepath")
460
+
461
+ t3_btn.click(
462
+ fn=process_span_prompting,
463
+ inputs=[t3_input, t3_desc, t3_anchors],
464
+ outputs=[t3_target, t3_residual, t3_status]
465
+ )
466
+
467
  if __name__ == "__main__":
468
  demo.launch(theme=gr.themes.Soft(
469
  primary_hue="blue",