Wataru commited on
Commit
59ed665
·
1 Parent(s): 371023d
Files changed (1) hide show
  1. app.py +62 -131
app.py CHANGED
@@ -1,6 +1,5 @@
1
  #!/usr/bin/env python3
2
  """DialogueSidon — two-speaker dialogue separation demo.
3
-
4
  Loads exported torch.export components from sarulab-speech/DialogueSidon on
5
  Hugging Face Hub and runs diffusion-based speaker separation.
6
  Inputs up to 120 s are processed in one shot; longer inputs use chunked
@@ -12,7 +11,6 @@ import json
12
  import os
13
  import subprocess
14
  import tempfile
15
- import time
16
 
17
  try:
18
  import spaces
@@ -96,20 +94,20 @@ def extract_fbank_features(
96
  mean = feat.mean(0, keepdim=True)
97
  var = feat.var(0, keepdim=True)
98
  feat = (feat - mean) / torch.sqrt(var + 1e-5)
99
- features.append(feat.to(device=device, dtype=torch.float32))
100
 
101
  input_features, attention_mask = _pad_batch(features)
102
  b, t, c = input_features.shape
103
  t = (t // stride) * stride
104
  input_features = input_features[:, :t, :]
105
  attention_mask = attention_mask[:, :t]
106
- input_features = input_features.reshape(b, t // stride, c * stride).contiguous()
107
- attention_mask = attention_mask[:, 1::stride].contiguous()
108
  return {"input_features": input_features, "attention_mask": attention_mask}
109
 
110
 
111
  # ---------------------------------------------------------------------------
112
- # Model loading (cached per device)
113
  # ---------------------------------------------------------------------------
114
 
115
  _cache: dict = {}
@@ -126,7 +124,6 @@ def load_models(device: torch.device) -> dict:
126
  with open(paths["metadata.json"]) as fp:
127
  meta = json.load(fp)
128
 
129
- print(f"Loading models onto {device} ...")
130
  ssl_encoder = torch.export.load(paths["ssl_encoder.pt2"]).module().to(device)
131
  diffusion_head = torch.export.load(paths["diffusion_head.pt2"]).module().to(device)
132
  vae_decoder = torch.export.load(paths["vae_decoder.pt2"]).module().to(device)
@@ -186,10 +183,9 @@ def _separate_chunk(
186
  latent_dim = models["latent_dim"]
187
 
188
  noisy_ssl = extract_fbank_features([wav.view(-1)], device)
189
- input_features = noisy_ssl["input_features"].to(device=device, dtype=torch.float32).contiguous()
190
- attention_mask = noisy_ssl["attention_mask"].to(device=device, dtype=torch.int64).contiguous()
191
-
192
- features, pred0, pred1 = models["ssl_encoder"](input_features)
193
 
194
  predicted_latents = torch.cat([pred0, pred1], dim=-1)
195
  conditioning = torch.cat([_normalize(predicted_latents, models), features], dim=-1)
@@ -207,7 +203,7 @@ def _separate_chunk(
207
  ).prev_sample
208
 
209
  latents = _denormalize(latents, models)
210
- spk1 = models["vae_decoder"](latents[:, :, :latent_dim].transpose(1, 2)).squeeze(0)
211
  spk2 = models["vae_decoder"](latents[:, :, latent_dim:].transpose(1, 2)).squeeze(0)
212
  return torch.cat([spk1, spk2], dim=0) # [2, T]
213
 
@@ -241,6 +237,7 @@ def separate(
241
  models = load_models(device)
242
  out_sr = models["sample_rate"]
243
 
 
244
  if sample_rate != SAMPLE_RATE_IN:
245
  wav_16k = torchaudio.functional.resample(wav, sample_rate, SAMPLE_RATE_IN)
246
  else:
@@ -251,11 +248,13 @@ def separate(
251
  total_samples = wav_16k.shape[-1]
252
 
253
  if total_samples <= chunk_samples:
 
254
  max_val = wav_16k.abs().max().clamp_min(1e-6)
255
  wav_norm = torch.nn.functional.pad(0.9 * wav_16k / max_val, (160, 160))
256
  separated = _separate_chunk(wav_norm, num_steps, models, device)
257
  return separated, out_sr
258
 
 
259
  overlap_samples_in = int(OVERLAP_SECONDS * SAMPLE_RATE_IN)
260
  hop_samples = chunk_samples - overlap_samples_in
261
  starts = list(range(0, total_samples, hop_samples))
@@ -263,14 +262,15 @@ def separate(
263
  stitched: torch.Tensor | None = None
264
  prev_end_in = 0
265
 
266
- for start in starts:
267
  end = min(start + chunk_samples, total_samples)
268
  chunk = wav_16k[:, start:end]
269
  max_val = chunk.abs().max().clamp_min(1e-6)
270
  chunk_norm = torch.nn.functional.pad(0.9 * chunk / max_val, (160, 160))
271
 
272
- pred = _separate_chunk(chunk_norm, num_steps, models, device)
273
 
 
274
  target_out = max(1, round((end - start) * out_sr / SAMPLE_RATE_IN))
275
  if pred.shape[-1] > target_out:
276
  pred = pred[:, :target_out]
@@ -284,14 +284,11 @@ def separate(
284
  continue
285
 
286
  overlap_in = max(0, prev_end_in - start)
287
- overlap_out = max(
288
- 0,
289
- min(
290
- round(overlap_in * out_sr / SAMPLE_RATE_IN),
291
- stitched.shape[-1],
292
- pred.shape[-1],
293
- ),
294
- )
295
 
296
  if overlap_out > 0:
297
  pred, _ = _maybe_swap(stitched[:, -overlap_out:], pred, overlap_out)
@@ -315,7 +312,8 @@ def extract_audio_from_video(video_path: str) -> tuple[torch.Tensor, int]:
315
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
316
  tmp_path = tmp.name
317
  subprocess.run(
318
- ["ffmpeg", "-y", "-i", video_path, "-ac", "1", "-ar", str(SAMPLE_RATE_IN), "-vn", tmp_path],
 
319
  check=True,
320
  stdout=subprocess.DEVNULL,
321
  stderr=subprocess.DEVNULL,
@@ -331,9 +329,13 @@ def create_stereo_video(
331
  spk2: np.ndarray,
332
  out_sr: int,
333
  ) -> str:
334
- """Mux separated speakers (L=spk1, R=spk2) back into a video file."""
335
- stereo = np.stack([spk1, spk2], axis=0)
 
 
 
336
  stereo_tensor = torch.from_numpy(stereo).float()
 
337
  peak = stereo_tensor.abs().max().clamp_min(1e-6)
338
  stereo_tensor = stereo_tensor / peak * 0.9
339
 
@@ -347,13 +349,13 @@ def create_stereo_video(
347
  subprocess.run(
348
  [
349
  "ffmpeg", "-y",
350
- "-i", video_path,
351
- "-i", audio_path,
352
- "-c:v", "copy",
353
- "-c:a", "aac",
354
  "-b:a", "192k",
355
- "-map", "0:v:0",
356
- "-map", "1:a:0",
357
  "-shortest",
358
  out_path,
359
  ],
@@ -365,45 +367,6 @@ def create_stereo_video(
365
  return out_path
366
 
367
 
368
- # ---------------------------------------------------------------------------
369
- # RTF report & mel helpers
370
- # ---------------------------------------------------------------------------
371
-
372
- def _extract_mel_db(wav_np: np.ndarray, sr: int, n_mels: int = 80) -> np.ndarray:
373
- wav_t = torch.from_numpy(wav_np.copy()).float()
374
- if wav_t.ndim != 1:
375
- wav_t = wav_t.reshape(-1)
376
- peak = wav_t.abs().max().clamp_min(1e-6)
377
- wav_t = (wav_t / peak).unsqueeze(0)
378
- mel_tf = torchaudio.transforms.MelSpectrogram(
379
- sample_rate=sr, n_fft=1024, hop_length=256, n_mels=n_mels
380
- )
381
- mel = mel_tf(wav_t)
382
- mel_db = torchaudio.transforms.AmplitudeToDB(stype="power", top_db=80)(mel)
383
- return mel_db[0].numpy()
384
-
385
-
386
- def _rtf_report(duration_s: float, proc_time_s: float, mel_stats: dict, device: str = "unknown") -> str:
387
- rtf = proc_time_s / max(duration_s, 1e-6)
388
- lines = [
389
- "=== DialogueSidon Separation Report ===",
390
- "",
391
- f"Device : {device}",
392
- f"Input duration : {duration_s:.2f} s",
393
- f"Processing time : {proc_time_s:.2f} s",
394
- f"RTF : {rtf:.3f}x ({'faster' if rtf < 1.0 else 'slower'} than real-time)",
395
- "",
396
- "--- Mel Spectrogram Statistics (80 bins) ---",
397
- f"{'Source':<12} {'Mean (dB)':>12} {'Peak (dB)':>12} {'Std (dB)':>12}",
398
- "-" * 52,
399
- ]
400
- for name, stats in mel_stats.items():
401
- lines.append(
402
- f"{name:<12} {stats['mean']:>12.1f} {stats['peak']:>12.1f} {stats['std']:>12.1f}"
403
- )
404
- return "\n".join(lines)
405
-
406
-
407
  # ---------------------------------------------------------------------------
408
  # Gradio interface
409
  # ---------------------------------------------------------------------------
@@ -413,7 +376,8 @@ def get_device() -> torch.device:
413
 
414
 
415
  def _wav_to_numpy_output(wav_tensor: torch.Tensor, sr: int) -> tuple[int, np.ndarray]:
416
- arr = wav_tensor.detach().cpu().numpy()
 
417
  arr = np.clip(arr / max(np.abs(arr).max(), 1e-6) * 0.9, -1.0, 1.0)
418
  return sr, (arr * 32767).astype(np.int16)
419
 
@@ -421,13 +385,11 @@ def _wav_to_numpy_output(wav_tensor: torch.Tensor, sr: int) -> tuple[int, np.nda
421
  def run_separation_audio(
422
  input_audio: tuple[int, np.ndarray] | None,
423
  num_steps: int,
424
- ) -> tuple[tuple[int, np.ndarray], tuple[int, np.ndarray], str]:
425
  if input_audio is None:
426
  raise gr.Error("Please upload an audio file.")
427
 
428
  sr, audio_np = input_audio
429
- duration_s = audio_np.shape[0] / sr
430
-
431
  wav = torch.from_numpy(audio_np.copy()).float()
432
 
433
  if wav.ndim == 1:
@@ -437,68 +399,39 @@ def run_separation_audio(
437
  wav = wav.T
438
  wav = wav.mean(dim=0, keepdim=True)
439
 
440
- scale = float(np.iinfo(audio_np.dtype).max) if audio_np.dtype in (np.int16, np.int32) else 1.0
441
- if scale != 1.0:
442
- wav = wav / scale
443
 
444
  device = get_device()
 
445
 
446
- t0 = time.perf_counter()
447
- separated, out_sr = separate(wav, sr, int(num_steps), device)
448
- proc_time = time.perf_counter() - t0
449
-
450
- spk1_out = _wav_to_numpy_output(separated[0], out_sr)
451
- spk2_out = _wav_to_numpy_output(separated[1], out_sr)
452
-
453
- audio_mono = audio_np.mean(axis=-1) if audio_np.ndim == 2 else audio_np
454
- mel_in = _extract_mel_db((audio_mono / scale).astype(np.float32), sr)
455
- mel_s1 = _extract_mel_db(spk1_out[1].astype(np.float32) / 32767.0, out_sr)
456
- mel_s2 = _extract_mel_db(spk2_out[1].astype(np.float32) / 32767.0, out_sr)
457
-
458
- mel_stats = {
459
- "Input": {"mean": float(mel_in.mean()), "peak": float(mel_in.max()), "std": float(mel_in.std())},
460
- "Speaker 1": {"mean": float(mel_s1.mean()), "peak": float(mel_s1.max()), "std": float(mel_s1.std())},
461
- "Speaker 2": {"mean": float(mel_s2.mean()), "peak": float(mel_s2.max()), "std": float(mel_s2.std())},
462
- }
463
- report = _rtf_report(duration_s, proc_time, mel_stats, str(device))
464
-
465
- return spk1_out, spk2_out, report
466
 
467
 
468
  def run_separation_video(
469
  video_path: str | None,
470
  num_steps: int,
471
- ) -> tuple[str | None, tuple[int, np.ndarray], tuple[int, np.ndarray], str]:
472
  if video_path is None:
473
  raise gr.Error("Please upload a video file.")
474
 
475
  wav, sr = extract_audio_from_video(video_path)
476
- duration_s = wav.shape[-1] / sr
477
  device = get_device()
 
478
 
479
- t0 = time.perf_counter()
480
- separated, out_sr = separate(wav, sr, int(num_steps), device)
481
- proc_time = time.perf_counter() - t0
482
 
483
- spk1_np = separated[0].detach().cpu().numpy()
484
- spk2_np = separated[1].detach().cpu().numpy()
485
  out_video = create_stereo_video(video_path, spk1_np, spk2_np, out_sr)
486
 
487
- spk1_out = _wav_to_numpy_output(separated[0], out_sr)
488
- spk2_out = _wav_to_numpy_output(separated[1], out_sr)
489
-
490
- mel_in = _extract_mel_db(wav[0].detach().cpu().numpy(), sr)
491
- mel_s1 = _extract_mel_db(spk1_out[1].astype(np.float32) / 32767.0, out_sr)
492
- mel_s2 = _extract_mel_db(spk2_out[1].astype(np.float32) / 32767.0, out_sr)
493
-
494
- mel_stats = {
495
- "Input": {"mean": float(mel_in.mean()), "peak": float(mel_in.max()), "std": float(mel_in.std())},
496
- "Speaker 1": {"mean": float(mel_s1.mean()), "peak": float(mel_s1.max()), "std": float(mel_s1.std())},
497
- "Speaker 2": {"mean": float(mel_s2.mean()), "peak": float(mel_s2.max()), "std": float(mel_s2.std())},
498
- }
499
- report = _rtf_report(duration_s, proc_time, mel_stats, str(device))
500
-
501
- return out_video, spk1_out, spk2_out, report
502
 
503
 
504
  if HAS_SPACES:
@@ -513,10 +446,8 @@ with gr.Blocks(title="DialogueSidon — Dialogue Separation") as demo:
513
  Upload a degraded or noisy audio/video recording of a two-speaker conversation.
514
  DialogueSidon jointly **separates** the two speakers and **restores** clean, high-quality speech
515
  from the mixture — handling background noise, reverberation, and channel degradation in one pass.
516
-
517
  Inputs up to 120 s are processed in one shot. Longer inputs are processed in 120 s chunks
518
  with 10 s overlap crossfade and automatic speaker re-alignment across chunks.
519
-
520
  **Model**: [sarulab-speech/DialogueSidon](https://huggingface.co/sarulab-speech/DialogueSidon)
521
  """
522
  )
@@ -527,6 +458,9 @@ with gr.Blocks(title="DialogueSidon — Dialogue Separation") as demo:
527
  )
528
 
529
  with gr.Tabs():
 
 
 
530
  with gr.Tab("Audio"):
531
  with gr.Row():
532
  with gr.Column():
@@ -540,20 +474,20 @@ with gr.Blocks(title="DialogueSidon — Dialogue Separation") as demo:
540
  audio_spk1 = gr.Audio(label="Speaker 1", type="numpy")
541
  audio_spk2 = gr.Audio(label="Speaker 2", type="numpy")
542
 
543
- audio_report = gr.Textbox(
544
- label="RTF Report", lines=10, max_lines=15, interactive=False
545
- )
546
  audio_btn.click(
547
  fn=run_separation_audio,
548
  inputs=[audio_input, num_steps],
549
- outputs=[audio_spk1, audio_spk2, audio_report],
550
  )
551
  gr.Examples(
552
- examples=[["LDC2026S02_mono.wav", 100]],
553
- inputs=[audio_input, num_steps],
554
  label="Ex1"
555
  )
556
 
 
 
 
557
  with gr.Tab("Video"):
558
  with gr.Row():
559
  with gr.Column():
@@ -569,13 +503,10 @@ with gr.Blocks(title="DialogueSidon — Dialogue Separation") as demo:
569
  video_spk1 = gr.Audio(label="Speaker 1 (audio only)", type="numpy")
570
  video_spk2 = gr.Audio(label="Speaker 2 (audio only)", type="numpy")
571
 
572
- video_report = gr.Textbox(
573
- label="RTF Report", lines=10, max_lines=15, interactive=False
574
- )
575
  video_btn.click(
576
  fn=run_separation_video,
577
  inputs=[video_input, num_steps],
578
- outputs=[video_output, video_spk1, video_spk2, video_report],
579
  )
580
 
581
  gr.Markdown("---\n**License**: CC-BY-NC 4.0 — non-commercial use only.")
 
1
  #!/usr/bin/env python3
2
  """DialogueSidon — two-speaker dialogue separation demo.
 
3
  Loads exported torch.export components from sarulab-speech/DialogueSidon on
4
  Hugging Face Hub and runs diffusion-based speaker separation.
5
  Inputs up to 120 s are processed in one shot; longer inputs use chunked
 
11
  import os
12
  import subprocess
13
  import tempfile
 
14
 
15
  try:
16
  import spaces
 
94
  mean = feat.mean(0, keepdim=True)
95
  var = feat.var(0, keepdim=True)
96
  feat = (feat - mean) / torch.sqrt(var + 1e-5)
97
+ features.append(feat.to(device))
98
 
99
  input_features, attention_mask = _pad_batch(features)
100
  b, t, c = input_features.shape
101
  t = (t // stride) * stride
102
  input_features = input_features[:, :t, :]
103
  attention_mask = attention_mask[:, :t]
104
+ input_features = input_features.reshape(b, t // stride, c * stride)
105
+ attention_mask = attention_mask[:, 1::stride]
106
  return {"input_features": input_features, "attention_mask": attention_mask}
107
 
108
 
109
  # ---------------------------------------------------------------------------
110
+ # Model loading (cached)
111
  # ---------------------------------------------------------------------------
112
 
113
  _cache: dict = {}
 
124
  with open(paths["metadata.json"]) as fp:
125
  meta = json.load(fp)
126
 
 
127
  ssl_encoder = torch.export.load(paths["ssl_encoder.pt2"]).module().to(device)
128
  diffusion_head = torch.export.load(paths["diffusion_head.pt2"]).module().to(device)
129
  vae_decoder = torch.export.load(paths["vae_decoder.pt2"]).module().to(device)
 
183
  latent_dim = models["latent_dim"]
184
 
185
  noisy_ssl = extract_fbank_features([wav.view(-1)], device)
186
+ features, pred0, pred1 = models["ssl_encoder"](
187
+ noisy_ssl["input_features"], noisy_ssl["attention_mask"]
188
+ )
 
189
 
190
  predicted_latents = torch.cat([pred0, pred1], dim=-1)
191
  conditioning = torch.cat([_normalize(predicted_latents, models), features], dim=-1)
 
203
  ).prev_sample
204
 
205
  latents = _denormalize(latents, models)
206
+ spk1 = models["vae_decoder"](latents[:, :, :latent_dim].transpose(1, 2)).squeeze(0) # [1, T]
207
  spk2 = models["vae_decoder"](latents[:, :, latent_dim:].transpose(1, 2)).squeeze(0)
208
  return torch.cat([spk1, spk2], dim=0) # [2, T]
209
 
 
237
  models = load_models(device)
238
  out_sr = models["sample_rate"]
239
 
240
+ # resample to 16 kHz
241
  if sample_rate != SAMPLE_RATE_IN:
242
  wav_16k = torchaudio.functional.resample(wav, sample_rate, SAMPLE_RATE_IN)
243
  else:
 
248
  total_samples = wav_16k.shape[-1]
249
 
250
  if total_samples <= chunk_samples:
251
+ # single-shot inference
252
  max_val = wav_16k.abs().max().clamp_min(1e-6)
253
  wav_norm = torch.nn.functional.pad(0.9 * wav_16k / max_val, (160, 160))
254
  separated = _separate_chunk(wav_norm, num_steps, models, device)
255
  return separated, out_sr
256
 
257
+ # chunked streaming inference
258
  overlap_samples_in = int(OVERLAP_SECONDS * SAMPLE_RATE_IN)
259
  hop_samples = chunk_samples - overlap_samples_in
260
  starts = list(range(0, total_samples, hop_samples))
 
262
  stitched: torch.Tensor | None = None
263
  prev_end_in = 0
264
 
265
+ for idx, start in enumerate(starts):
266
  end = min(start + chunk_samples, total_samples)
267
  chunk = wav_16k[:, start:end]
268
  max_val = chunk.abs().max().clamp_min(1e-6)
269
  chunk_norm = torch.nn.functional.pad(0.9 * chunk / max_val, (160, 160))
270
 
271
+ pred = _separate_chunk(chunk_norm, num_steps, models, device) # [2, T_out]
272
 
273
+ # match output length to input length (resampling ratio)
274
  target_out = max(1, round((end - start) * out_sr / SAMPLE_RATE_IN))
275
  if pred.shape[-1] > target_out:
276
  pred = pred[:, :target_out]
 
284
  continue
285
 
286
  overlap_in = max(0, prev_end_in - start)
287
+ overlap_out = max(0, min(
288
+ round(overlap_in * out_sr / SAMPLE_RATE_IN),
289
+ stitched.shape[-1],
290
+ pred.shape[-1],
291
+ ))
 
 
 
292
 
293
  if overlap_out > 0:
294
  pred, _ = _maybe_swap(stitched[:, -overlap_out:], pred, overlap_out)
 
312
  with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
313
  tmp_path = tmp.name
314
  subprocess.run(
315
+ ["ffmpeg", "-y", "-i", video_path, "-ac", "1", "-ar", str(SAMPLE_RATE_IN),
316
+ "-vn", tmp_path],
317
  check=True,
318
  stdout=subprocess.DEVNULL,
319
  stderr=subprocess.DEVNULL,
 
329
  spk2: np.ndarray,
330
  out_sr: int,
331
  ) -> str:
332
+ """Mux separated speakers (L=spk1, R=spk2) back into a video file.
333
+ Returns path to the output video (caller is responsible for cleanup).
334
+ """
335
+ # Write stereo audio to a temp wav
336
+ stereo = np.stack([spk1, spk2], axis=0) # [2, T]
337
  stereo_tensor = torch.from_numpy(stereo).float()
338
+ # Normalise to avoid clipping
339
  peak = stereo_tensor.abs().max().clamp_min(1e-6)
340
  stereo_tensor = stereo_tensor / peak * 0.9
341
 
 
349
  subprocess.run(
350
  [
351
  "ffmpeg", "-y",
352
+ "-i", video_path, # original video (video stream)
353
+ "-i", audio_path, # new stereo audio
354
+ "-c:v", "copy", # copy video stream unchanged
355
+ "-c:a", "aac", # encode audio as AAC
356
  "-b:a", "192k",
357
+ "-map", "0:v:0", # take video from first input
358
+ "-map", "1:a:0", # take audio from second input
359
  "-shortest",
360
  out_path,
361
  ],
 
367
  return out_path
368
 
369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
370
  # ---------------------------------------------------------------------------
371
  # Gradio interface
372
  # ---------------------------------------------------------------------------
 
376
 
377
 
378
  def _wav_to_numpy_output(wav_tensor: torch.Tensor, sr: int) -> tuple[int, np.ndarray]:
379
+ arr = wav_tensor.cpu().numpy()
380
+ # Convert to int16 for Gradio audio output
381
  arr = np.clip(arr / max(np.abs(arr).max(), 1e-6) * 0.9, -1.0, 1.0)
382
  return sr, (arr * 32767).astype(np.int16)
383
 
 
385
  def run_separation_audio(
386
  input_audio: tuple[int, np.ndarray] | None,
387
  num_steps: int,
388
+ ) -> tuple[tuple[int, np.ndarray], tuple[int, np.ndarray]]:
389
  if input_audio is None:
390
  raise gr.Error("Please upload an audio file.")
391
 
392
  sr, audio_np = input_audio
 
 
393
  wav = torch.from_numpy(audio_np.copy()).float()
394
 
395
  if wav.ndim == 1:
 
399
  wav = wav.T
400
  wav = wav.mean(dim=0, keepdim=True)
401
 
402
+ if audio_np.dtype in (np.int16, np.int32):
403
+ wav = wav / float(np.iinfo(audio_np.dtype).max)
 
404
 
405
  device = get_device()
406
+ separated, out_sr = separate(wav, sr, num_steps, device)
407
 
408
+ return (
409
+ _wav_to_numpy_output(separated[0], out_sr),
410
+ _wav_to_numpy_output(separated[1], out_sr),
411
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
412
 
413
 
414
  def run_separation_video(
415
  video_path: str | None,
416
  num_steps: int,
417
+ ) -> tuple[str | None, tuple[int, np.ndarray], tuple[int, np.ndarray]]:
418
  if video_path is None:
419
  raise gr.Error("Please upload a video file.")
420
 
421
  wav, sr = extract_audio_from_video(video_path)
 
422
  device = get_device()
423
+ separated, out_sr = separate(wav, sr, num_steps, device)
424
 
425
+ spk1_np = separated[0].cpu().numpy()
426
+ spk2_np = separated[1].cpu().numpy()
 
427
 
 
 
428
  out_video = create_stereo_video(video_path, spk1_np, spk2_np, out_sr)
429
 
430
+ return (
431
+ out_video,
432
+ _wav_to_numpy_output(separated[0], out_sr),
433
+ _wav_to_numpy_output(separated[1], out_sr),
434
+ )
 
 
 
 
 
 
 
 
 
 
435
 
436
 
437
  if HAS_SPACES:
 
446
  Upload a degraded or noisy audio/video recording of a two-speaker conversation.
447
  DialogueSidon jointly **separates** the two speakers and **restores** clean, high-quality speech
448
  from the mixture — handling background noise, reverberation, and channel degradation in one pass.
 
449
  Inputs up to 120 s are processed in one shot. Longer inputs are processed in 120 s chunks
450
  with 10 s overlap crossfade and automatic speaker re-alignment across chunks.
 
451
  **Model**: [sarulab-speech/DialogueSidon](https://huggingface.co/sarulab-speech/DialogueSidon)
452
  """
453
  )
 
458
  )
459
 
460
  with gr.Tabs():
461
+ # ------------------------------------------------------------------
462
+ # Audio tab
463
+ # ------------------------------------------------------------------
464
  with gr.Tab("Audio"):
465
  with gr.Row():
466
  with gr.Column():
 
474
  audio_spk1 = gr.Audio(label="Speaker 1", type="numpy")
475
  audio_spk2 = gr.Audio(label="Speaker 2", type="numpy")
476
 
 
 
 
477
  audio_btn.click(
478
  fn=run_separation_audio,
479
  inputs=[audio_input, num_steps],
480
+ outputs=[audio_spk1, audio_spk2],
481
  )
482
  gr.Examples(
483
+ examples=[["LDC2026S02_mono.wav",100]],
484
+ inputs=[audio_input,num_steps],
485
  label="Ex1"
486
  )
487
 
488
+ # ------------------------------------------------------------------
489
+ # Video tab
490
+ # ------------------------------------------------------------------
491
  with gr.Tab("Video"):
492
  with gr.Row():
493
  with gr.Column():
 
503
  video_spk1 = gr.Audio(label="Speaker 1 (audio only)", type="numpy")
504
  video_spk2 = gr.Audio(label="Speaker 2 (audio only)", type="numpy")
505
 
 
 
 
506
  video_btn.click(
507
  fn=run_separation_video,
508
  inputs=[video_input, num_steps],
509
+ outputs=[video_output, video_spk1, video_spk2],
510
  )
511
 
512
  gr.Markdown("---\n**License**: CC-BY-NC 4.0 — non-commercial use only.")