xiaoyao9184 commited on
Commit
97f5f6f
·
verified ·
1 Parent(s): cb1f35f

Synced repo using 'sync_with_huggingface' Github Action

Browse files
Files changed (3) hide show
  1. app.py +1 -1
  2. gradio_app.py +147 -62
  3. requirements.txt +5 -4
app.py CHANGED
@@ -6,7 +6,7 @@ import subprocess
6
  from huggingface_hub import hf_hub_download
7
 
8
  REPO_URL = "https://github.com/facebookresearch/videoseal.git"
9
- REPO_BRANCH = '5897ac50b5b0f5c806f42d2f7d1ef208a0780a28'
10
  LOCAL_PATH = "./videoseal"
11
 
12
  def install_src():
 
6
  from huggingface_hub import hf_hub_download
7
 
8
  REPO_URL = "https://github.com/facebookresearch/videoseal.git"
9
+ REPO_BRANCH = '3de6b246bd160240c0b45790bb9b3a797eb7583a'
10
  LOCAL_PATH = "./videoseal"
11
 
12
  def install_src():
gradio_app.py CHANGED
@@ -28,14 +28,22 @@ import videoseal
28
  from videoseal.utils.display import save_video_audio_to_mp4
29
 
30
  # Load video_model if not already loaded in reload mode
31
- if 'video_model' not in globals():
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
- # Load the VideoSeal model
35
- video_model = videoseal.load("videoseal")
 
 
36
  video_model.eval()
37
  video_model.to(device)
38
- video_model_nbytes = int(video_model.embedder.msg_processor.nbits / 8)
 
 
 
 
 
 
39
 
40
  # Load the AudioSeal model
41
  # Load audio_generator if not already loaded in reload mode
@@ -49,6 +57,10 @@ if 'audio_detector' not in globals():
49
  audio_detector = AudioSeal.load_detector("audioseal_detector_16bits")
50
  audio_detector = audio_detector.to(device)
51
 
 
 
 
 
52
  def generate_msg_pt_by_format_string(format_string, bytes_count):
53
  msg_hex = format_string.replace("-", "")
54
  hex_length = bytes_count * 2
@@ -345,8 +357,9 @@ def embed_audio(
345
  # print(stderr_output2)
346
  return
347
 
348
- def embed_watermark(input_path, output_path, msg_v, msg_a, video_only, progress):
349
  output_path_video = output_path + ".video.mp4"
 
350
  embed_video(video_model, input_path, output_path_video, msg_v, 16)
351
 
352
  output_path_audio = output_path + ".audio.m4a"
@@ -378,6 +391,7 @@ def detect_video_clip(
378
 
379
  def detect_video(
380
  model,
 
381
  input_path: str,
382
  chunk_size: int
383
  ) -> None:
@@ -402,7 +416,7 @@ def detect_video(
402
  chunk = np.zeros((chunk_size, height, width, 3), dtype=np.uint8)
403
  frame_count = 0
404
  soft_msgs = []
405
- pbar = tqdm.tqdm(total=num_frames, unit='frame', desc="Watermark video detecting")
406
  while True:
407
  in_bytes = process1.stdout.read(frame_size)
408
  if not in_bytes:
@@ -521,16 +535,25 @@ def detect_audio(
521
  soft_message_prob = torch.cat(soft_message_prob, dim=0)
522
  return (soft_result, soft_message, soft_pred_prob, soft_message_prob)
523
 
524
- def detect_watermark(input_path, video_only):
525
- msgs_v_frame = detect_video(video_model, input_path, 16)
526
- msgs_v_avg = msgs_v_frame.mean(dim=0) # Average the predictions across all frames
527
- msgs_v_frame = (msgs_v_frame > 0).to(int)
528
- msgs_v_avg = (msgs_v_avg > 0).to(int)
529
- msgs_v_unique, msgs_v_counts = torch.unique(msgs_v_frame, dim=0, return_counts=True)
530
- msgs_v_most = None
531
- if len(msgs_v_frame) > len(msgs_v_counts) > 0:
532
- msgs_v_most_idx = torch.argmax(msgs_v_counts)
533
- msgs_v_most = msgs_v_unique[msgs_v_most_idx]
 
 
 
 
 
 
 
 
 
534
 
535
  msgs_a_most = msgs_a_res = msgs_a_frame = msgs_a_pred = msgs_a_prob = None
536
  if not video_only:
@@ -549,7 +572,7 @@ def detect_watermark(input_path, video_only):
549
  with gr.Blocks(title="VideoSeal") as demo:
550
  gr.Markdown("""
551
  # VideoSeal Demo
552
-
553
  For video, each frame will be watermarked and detected.
554
  For audio, each 3 seconds will be watermarked, and each second will be detected.
555
 
@@ -570,7 +593,8 @@ with gr.Blocks(title="VideoSeal") as demo:
570
  with gr.Column():
571
  embedding_type = gr.Radio(["random", "input"], value="random", label="Type", info="Type of watermarks")
572
 
573
- format_like_v, regex_pattern_v = generate_hex_format_regex(video_model_nbytes)
 
574
  msg_v, _ = generate_hex_random_message(video_model_nbytes)
575
  embedding_msg_v = gr.Textbox(
576
  label=f"Message ({video_model_nbytes} bytes hex string)",
@@ -578,42 +602,64 @@ with gr.Blocks(title="VideoSeal") as demo:
578
  value=msg_v,
579
  interactive=False, show_copy_button=True)
580
  with gr.Column():
581
- embedding_only_vid = gr.Checkbox(label="Only Video", value=False)
582
-
583
- format_like_a, regex_pattern_a = generate_hex_format_regex(audio_generator_nbytes)
584
- msg_a, _ = generate_hex_random_message(audio_generator_nbytes)
585
- embedding_msg_a = gr.Textbox(
586
- label=f"Audio Message ({audio_generator_nbytes} bytes hex string)",
587
- info=f"format like {format_like_a}",
588
- value=msg_a,
589
- interactive=False, show_copy_button=True)
590
-
 
591
  embedding_btn = gr.Button("Embed Watermark")
592
  with gr.Column():
593
  marked_vid = gr.Video(label="Output Audio", show_download_button=True)
594
 
595
- def change_embedding_type(video_only):
596
  return gr.update(visible=not video_only)
597
  embedding_only_vid.change(
598
- fn=change_embedding_type,
599
  inputs=[embedding_only_vid],
600
- outputs=[embedding_msg_a]
 
601
  )
602
 
603
- def change_embedding_type(type):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
  if type == "random":
 
605
  msg_v, _ = generate_hex_random_message(video_model_nbytes)
606
- msg_a,_ = generate_hex_random_message(audio_generator_nbytes)
607
  return [gr.update(interactive=False, value=msg_v),gr.update(interactive=False, value=msg_a)]
608
  else:
609
  return [gr.update(interactive=True),gr.update(interactive=True)]
610
  embedding_type.change(
611
  fn=change_embedding_type,
612
- inputs=[embedding_type],
613
- outputs=[embedding_msg_v, embedding_msg_a]
 
614
  )
615
 
616
- def check_embedding_msg(msg_v, msg_a):
 
 
 
617
  if not re.match(regex_pattern_v, msg_v):
618
  gr.Warning(
619
  f"Invalid format. Please use like '{format_like_v}'",
@@ -624,17 +670,36 @@ with gr.Blocks(title="VideoSeal") as demo:
624
  duration=0)
625
  embedding_msg_v.change(
626
  fn=check_embedding_msg,
627
- inputs=[embedding_msg_v, embedding_msg_a],
628
- outputs=[]
 
629
  )
630
  embedding_msg_a.change(
631
  fn=check_embedding_msg,
632
  inputs=[embedding_msg_v, embedding_msg_a],
633
- outputs=[]
 
634
  )
635
 
636
- def run_embed_watermark(input_path, video_only, msg_v, msg_a, progress=gr.Progress(track_tqdm=True)):
637
- if input_path is None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
  raise gr.Error("No file uploaded", duration=5)
639
  if not re.match(regex_pattern_v, msg_v):
640
  raise gr.Error(f"Invalid format. Please use like '{format_like_v}'", duration=5)
@@ -645,15 +710,15 @@ with gr.Blocks(title="VideoSeal") as demo:
645
  msg_pt_a = generate_msg_pt_by_format_string(msg_a, audio_generator_nbytes)
646
 
647
  if video_only:
648
- output_path = os.path.join(os.path.dirname(input_path), "__".join([msg_v]) + '.mp4')
649
  else:
650
- output_path = os.path.join(os.path.dirname(input_path), "__".join([msg_v, msg_a]) + '.mp4')
651
- embed_watermark(input_path, output_path, msg_pt_v, msg_pt_a, video_only, progress)
652
 
653
  return output_path
654
  embedding_btn.click(
655
  fn=run_embed_watermark,
656
- inputs=[embedding_vid, embedding_only_vid, embedding_msg_v, embedding_msg_a],
657
  outputs=[marked_vid]
658
  )
659
 
@@ -661,28 +726,48 @@ with gr.Blocks(title="VideoSeal") as demo:
661
  with gr.Row():
662
  with gr.Column():
663
  detecting_vid = gr.Video(label="Input Video")
664
- detecting_only_vid = gr.Checkbox(label="Only Video", value=False)
 
 
665
  detecting_btn = gr.Button("Detect Watermark")
666
  with gr.Column():
667
  predicted_messages = gr.JSON(label="Detected Messages")
668
 
669
- def run_detect_watermark(file, video_only, progress=gr.Progress(track_tqdm=True)):
 
 
 
 
 
 
 
 
 
 
 
 
670
  if file is None:
671
  raise gr.Error("No file uploaded", duration=5)
672
 
673
- msgs_v_most, msgs_v_avg, msgs_v_frame, msgs_a_most, msgs_a_res, msgs_a_frame, msgs_a_pred, msgs_a_prob = detect_watermark(file, video_only)
674
-
675
- _, format_msg_v_most = generate_format_string_by_msg_pt(msgs_v_most, video_model_nbytes)
676
- _, format_msg_v_avg = generate_format_string_by_msg_pt(msgs_v_avg, video_model_nbytes)
677
- format_msg_v_frames = {}
678
- for idx, msg in enumerate(msgs_v_frame):
679
- _, format_msg = generate_format_string_by_msg_pt(msg, video_model_nbytes)
680
- format_msg_v_frames[f"{idx}"] = format_msg
681
- video_json = {
682
- "most": format_msg_v_most,
683
- "avg": format_msg_v_avg,
684
- "frames": format_msg_v_frames
685
- }
 
 
 
 
 
 
686
 
687
  if msgs_a_res is None:
688
  audio_json = None
@@ -714,9 +799,9 @@ with gr.Blocks(title="VideoSeal") as demo:
714
  return message_json
715
  detecting_btn.click(
716
  fn=run_detect_watermark,
717
- inputs=[detecting_vid, detecting_only_vid],
718
  outputs=[predicted_messages]
719
  )
720
 
721
  if __name__ == "__main__":
722
- demo.launch()
 
28
  from videoseal.utils.display import save_video_audio_to_mp4
29
 
30
  # Load video_model if not already loaded in reload mode
31
+ if 'video_models' not in globals():
32
  device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
33
 
34
+ video_models = {}
35
+
36
+ # Load the VideoSeal model 1.0
37
+ video_model = videoseal.load("videoseal_1.0")
38
  video_model.eval()
39
  video_model.to(device)
40
+ video_models['1.0'] = video_model
41
+
42
+ # Load the VideoSeal model 0.0
43
+ video_model = videoseal.load("videoseal_0.0")
44
+ video_model.eval()
45
+ video_model.to(device)
46
+ video_models['0.0'] = video_model
47
 
48
  # Load the AudioSeal model
49
  # Load audio_generator if not already loaded in reload mode
 
57
  audio_detector = AudioSeal.load_detector("audioseal_detector_16bits")
58
  audio_detector = audio_detector.to(device)
59
 
60
+ def get_model_nbytes(model_version):
61
+ video_model = video_models[model_version]
62
+ return int(video_model.embedder.msg_processor.nbits / 8)
63
+
64
  def generate_msg_pt_by_format_string(format_string, bytes_count):
65
  msg_hex = format_string.replace("-", "")
66
  hex_length = bytes_count * 2
 
357
  # print(stderr_output2)
358
  return
359
 
360
+ def embed_watermark(input_path, model_version, output_path, msg_v, msg_a, video_only, progress):
361
  output_path_video = output_path + ".video.mp4"
362
+ video_model = video_models[model_version]
363
  embed_video(video_model, input_path, output_path_video, msg_v, 16)
364
 
365
  output_path_audio = output_path + ".audio.m4a"
 
391
 
392
  def detect_video(
393
  model,
394
+ version: str,
395
  input_path: str,
396
  chunk_size: int
397
  ) -> None:
 
416
  chunk = np.zeros((chunk_size, height, width, 3), dtype=np.uint8)
417
  frame_count = 0
418
  soft_msgs = []
419
+ pbar = tqdm.tqdm(total=num_frames, unit='frame', desc=f"{version}: Watermark video detecting")
420
  while True:
421
  in_bytes = process1.stdout.read(frame_size)
422
  if not in_bytes:
 
535
  soft_message_prob = torch.cat(soft_message_prob, dim=0)
536
  return (soft_result, soft_message, soft_pred_prob, soft_message_prob)
537
 
538
+ def detect_watermark(input_path, version_keys, video_only):
539
+ msgs_v_most = {}
540
+ msgs_v_avg = {}
541
+ msgs_v_frame = {}
542
+ for video_version, video_model in video_models.items():
543
+ if video_version not in version_keys:
544
+ continue
545
+ version_msgs_v_frame = detect_video(video_model, video_version, input_path, 16)
546
+ version_msgs_v_frame = (version_msgs_v_frame > 0).to(int)
547
+ version_msgs_v_avg = (version_msgs_v_frame.to(torch.float32).mean(dim=0) > 0).to(int)
548
+ version_msgs_v_most = None
549
+ version_msgs_v_unique, version_msgs_v_counts = torch.unique(version_msgs_v_frame, dim=0, return_counts=True)
550
+ if len(version_msgs_v_frame) > len(version_msgs_v_counts) > 0:
551
+ version_msgs_v_most_idx = torch.argmax(version_msgs_v_counts)
552
+ version_msgs_v_most = version_msgs_v_unique[version_msgs_v_most_idx]
553
+
554
+ msgs_v_most[video_version] = version_msgs_v_most
555
+ msgs_v_avg[video_version] = version_msgs_v_avg
556
+ msgs_v_frame[video_version] = version_msgs_v_frame
557
 
558
  msgs_a_most = msgs_a_res = msgs_a_frame = msgs_a_pred = msgs_a_prob = None
559
  if not video_only:
 
572
  with gr.Blocks(title="VideoSeal") as demo:
573
  gr.Markdown("""
574
  # VideoSeal Demo
575
+ ![](https://badge.mcpx.dev?type=server 'MCP Server')
576
  For video, each frame will be watermarked and detected.
577
  For audio, each 3 seconds will be watermarked, and each second will be detected.
578
 
 
593
  with gr.Column():
594
  embedding_type = gr.Radio(["random", "input"], value="random", label="Type", info="Type of watermarks")
595
 
596
+ video_model_nbytes = get_model_nbytes(list(video_models.keys())[0])
597
+ format_like_v, _ = generate_hex_format_regex(video_model_nbytes)
598
  msg_v, _ = generate_hex_random_message(video_model_nbytes)
599
  embedding_msg_v = gr.Textbox(
600
  label=f"Message ({video_model_nbytes} bytes hex string)",
 
602
  value=msg_v,
603
  interactive=False, show_copy_button=True)
604
  with gr.Column():
605
+ embedding_version = gr.Dropdown(video_models.keys(), label="Model version", interactive=True)
606
+ with gr.Column():
607
+ embedding_only_vid = gr.Checkbox(label="Only Video", value=False)
608
+
609
+ format_like_a, _ = generate_hex_format_regex(audio_generator_nbytes)
610
+ msg_a, _ = generate_hex_random_message(audio_generator_nbytes)
611
+ embedding_msg_a = gr.Textbox(
612
+ label=f"Audio Message ({audio_generator_nbytes} bytes hex string)",
613
+ info=f"format like {format_like_a}",
614
+ value=msg_a,
615
+ interactive=False, show_copy_button=True)
616
  embedding_btn = gr.Button("Embed Watermark")
617
  with gr.Column():
618
  marked_vid = gr.Video(label="Output Audio", show_download_button=True)
619
 
620
+ def change_embedding_silent(video_only):
621
  return gr.update(visible=not video_only)
622
  embedding_only_vid.change(
623
+ fn=change_embedding_silent,
624
  inputs=[embedding_only_vid],
625
+ outputs=[embedding_msg_a],
626
+ api_name=False
627
  )
628
 
629
+ def change_embedding_version(version):
630
+ video_model_nbytes = get_model_nbytes(version)
631
+ format_like_v, _ = generate_hex_format_regex(video_model_nbytes)
632
+ msg_v, _ = generate_hex_random_message(video_model_nbytes)
633
+ return gr.update(
634
+ label=f"Message ({video_model_nbytes} bytes hex string)",
635
+ info=f"format like {format_like_v}",
636
+ value=msg_v)
637
+ embedding_version.change(
638
+ fn=change_embedding_version,
639
+ inputs=[embedding_version],
640
+ outputs=[embedding_msg_v],
641
+ api_name=False
642
+ )
643
+
644
+ def change_embedding_type(type, version):
645
  if type == "random":
646
+ video_model_nbytes = get_model_nbytes(version)
647
  msg_v, _ = generate_hex_random_message(video_model_nbytes)
648
+ msg_a, _ = generate_hex_random_message(audio_generator_nbytes)
649
  return [gr.update(interactive=False, value=msg_v),gr.update(interactive=False, value=msg_a)]
650
  else:
651
  return [gr.update(interactive=True),gr.update(interactive=True)]
652
  embedding_type.change(
653
  fn=change_embedding_type,
654
+ inputs=[embedding_type, embedding_version],
655
+ outputs=[embedding_msg_v, embedding_msg_a],
656
+ api_name=False
657
  )
658
 
659
+ def check_embedding_msg(version_v, msg_v, msg_a):
660
+ video_model_nbytes = get_model_nbytes(version_v)
661
+ _, regex_pattern_v = generate_hex_format_regex(video_model_nbytes)
662
+ _, regex_pattern_a = generate_hex_format_regex(audio_generator_nbytes)
663
  if not re.match(regex_pattern_v, msg_v):
664
  gr.Warning(
665
  f"Invalid format. Please use like '{format_like_v}'",
 
670
  duration=0)
671
  embedding_msg_v.change(
672
  fn=check_embedding_msg,
673
+ inputs=[embedding_version, embedding_msg_v, embedding_msg_a],
674
+ outputs=[],
675
+ api_name=False
676
  )
677
  embedding_msg_a.change(
678
  fn=check_embedding_msg,
679
  inputs=[embedding_msg_v, embedding_msg_a],
680
+ outputs=[],
681
+ api_name=False
682
  )
683
 
684
+ def run_embed_watermark(file, model_version, video_only, msg_v, msg_a, progress=gr.Progress(track_tqdm=True)):
685
+ """
686
+ Embeds a watermark into the given video file using the specified model.
687
+
688
+ Args:
689
+ file (str): Path to the input video file.
690
+ model_version (str): Identifier for the video model version or checkpoint used for embedding.
691
+ video_only (bool): If True, embeds watermark only in the video stream; audio is ignored.
692
+ msg_v (str): A 12- or 32-byte hexadecimal string to embed as a watermark in the video stream (e.g., "FFFF").
693
+ msg_a (str): A 2-byte hexadecimal string to embed as a watermark in the audio stream (e.g., "FFFF").
694
+ progress (gr.Progress, optional): Gradio progress tracker for monitoring embedding progress. Defaults to tracking tqdm.
695
+
696
+ Returns:
697
+ str: File path to the watermarked output video file.
698
+ """
699
+ video_model_nbytes = get_model_nbytes(model_version)
700
+ _, regex_pattern_v = generate_hex_format_regex(video_model_nbytes)
701
+ _, regex_pattern_a = generate_hex_format_regex(audio_generator_nbytes)
702
+ if file is None:
703
  raise gr.Error("No file uploaded", duration=5)
704
  if not re.match(regex_pattern_v, msg_v):
705
  raise gr.Error(f"Invalid format. Please use like '{format_like_v}'", duration=5)
 
710
  msg_pt_a = generate_msg_pt_by_format_string(msg_a, audio_generator_nbytes)
711
 
712
  if video_only:
713
+ output_path = os.path.join(os.path.dirname(file), "__".join([msg_v]) + '.mp4')
714
  else:
715
+ output_path = os.path.join(os.path.dirname(file), "__".join([msg_v, msg_a]) + '.mp4')
716
+ embed_watermark(file, model_version, output_path, msg_pt_v, msg_pt_a, video_only, progress)
717
 
718
  return output_path
719
  embedding_btn.click(
720
  fn=run_embed_watermark,
721
+ inputs=[embedding_vid, embedding_version, embedding_only_vid, embedding_msg_v, embedding_msg_a],
722
  outputs=[marked_vid]
723
  )
724
 
 
726
  with gr.Row():
727
  with gr.Column():
728
  detecting_vid = gr.Video(label="Input Video")
729
+ with gr.Row():
730
+ detecting_model_dd = gr.Dropdown(video_models.keys(), value=list(video_models.keys()), multiselect=True, label="Model version", interactive=True)
731
+ detecting_only_vid = gr.Checkbox(label="Only Video", value=False)
732
  detecting_btn = gr.Button("Detect Watermark")
733
  with gr.Column():
734
  predicted_messages = gr.JSON(label="Detected Messages")
735
 
736
+ def run_detect_watermark(file, model_versions, video_only, progress=gr.Progress(track_tqdm=True)):
737
+ """
738
+ Detects a watermark in the given video file using specified model versions.
739
+
740
+ Args:
741
+ file (str): Path to the input video file.
742
+ model_versions (List[str]): List of model version identifiers (e.g., checkpoint versions) to use for detection.
743
+ video_only (bool): If True, only the video stream is considered; audio is ignored.
744
+ progress (gr.Progress, optional): Gradio Progress tracker for visualizing progress. Defaults to tracking tqdm.
745
+
746
+ Returns:
747
+ str: A Markdown-formatted string containing the detection results.
748
+ """
749
  if file is None:
750
  raise gr.Error("No file uploaded", duration=5)
751
 
752
+ msgs_v_most, msgs_v_avg, msgs_v_frame, msgs_a_most, msgs_a_res, msgs_a_frame, msgs_a_pred, msgs_a_prob = detect_watermark(file, model_versions, video_only)
753
+
754
+ video_json = {}
755
+ for (version_name, version_msgs_v_most), (_, version_msgs_v_avg), (_, version_msgs_v_frame) in zip(msgs_v_most.items(), msgs_v_avg.items(), msgs_v_frame.items()):
756
+ if version_name not in model_versions:
757
+ continue
758
+
759
+ video_model_nbytes = get_model_nbytes(version_name)
760
+ _, format_msg_v_most = generate_format_string_by_msg_pt(version_msgs_v_most, video_model_nbytes)
761
+ _, format_msg_v_avg = generate_format_string_by_msg_pt(version_msgs_v_avg, video_model_nbytes)
762
+ format_msg_v_frames = {}
763
+ for idx, msg in enumerate(version_msgs_v_frame):
764
+ _, format_msg = generate_format_string_by_msg_pt(msg, video_model_nbytes)
765
+ format_msg_v_frames[f"{idx}"] = format_msg
766
+ video_json[version_name] = {
767
+ "most": format_msg_v_most,
768
+ "avg": format_msg_v_avg,
769
+ "frames": format_msg_v_frames
770
+ }
771
 
772
  if msgs_a_res is None:
773
  audio_json = None
 
799
  return message_json
800
  detecting_btn.click(
801
  fn=run_detect_watermark,
802
+ inputs=[detecting_vid, detecting_model_dd, detecting_only_vid],
803
  outputs=[predicted_messages]
804
  )
805
 
806
  if __name__ == "__main__":
807
+ demo.launch(server_name="0.0.0.0", server_port=7860, mcp_server=True, ssr_mode=False)
requirements.txt CHANGED
@@ -1,10 +1,11 @@
1
  torch==2.5.1
2
- gradio==5.8.0
3
  GitPython==3.1.43
4
- huggingface-hub==0.26.3
5
  audioseal==0.1.4
6
  matplotlib==3.10.0
7
  soundfile==0.12.1
8
  torchaudio==2.5.1
9
- # see https://github.com/gradio-app/gradio/issues/10649
10
- pydantic==2.10.6
 
 
1
  torch==2.5.1
2
+ gradio[mcp]==5.28.0
3
  GitPython==3.1.43
4
+ huggingface-hub==0.28.1
5
  audioseal==0.1.4
6
  matplotlib==3.10.0
7
  soundfile==0.12.1
8
  torchaudio==2.5.1
9
+
10
+ # gradio[mcp] 5.28.0 depends on pydantic>=2.11
11
+ pydantic==2.11.4