Secking Claude commited on
Commit
bd7ca9e
·
1 Parent(s): a47571f

Add ZIP download functionality for segment alpha masks with loading modal

Browse files

Implements download feature for Segmentation Editing tab that packages only alpha mask PNG files into a ZIP archive named with the segment UUID.

- Modified download_segment() to include only alpha masks (no frame images)
- Changed ZIP filename from segment_{uuid}.zip to {uuid}.zip
- Added custom loading modal overlay with spinner and "Preparing download…" text
- Added button interactivity: disabled when no segment/masks, enabled when masks exist
- Added status feedback textbox showing download success or error messages
- JavaScript automatically shows/hides modal during download process

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +153 -30
app.py CHANGED
@@ -445,7 +445,7 @@ def handle_segment_selection(segment_id, magic_code):
445
  Handle segment selection: download all files and initialize the view.
446
  """
447
  if not segment_id or not magic_code:
448
- return None, gr.update(maximum=0, value=0), {}, {}, magic_code
449
 
450
  logger.info(f"Segment selected: {segment_id}")
451
 
@@ -454,7 +454,7 @@ def handle_segment_selection(segment_id, magic_code):
454
 
455
  if not frames_dict:
456
  logger.error("No frames downloaded")
457
- return None, gr.update(maximum=0, value=0), {}, {}, magic_code
458
 
459
  # Load first frame
460
  frame_0 = frames_dict.get(0, None)
@@ -465,12 +465,16 @@ def handle_segment_selection(segment_id, magic_code):
465
  # Initial display without mask
466
  result_image = composite_image_with_mask(frame_0, masks_dict.get(0, None), False)
467
 
 
 
 
468
  return (
469
  result_image,
470
  gr.update(minimum=0, maximum=max_frame, value=0),
471
  frames_dict,
472
  masks_dict,
473
- magic_code
 
474
  )
475
 
476
 
@@ -479,7 +483,7 @@ def handle_image_click(segment_id, frame_number, magic_code, frames_state, masks
479
  Handle click on ImageEditor: send coordinates to SAM-3, get new mask, update display.
480
  """
481
  if not segment_id or frames_state is None or evt is None:
482
- return None, masks_state
483
 
484
  # Extract click coordinates
485
  x, y = evt.index[0], evt.index[1]
@@ -489,7 +493,7 @@ def handle_image_click(segment_id, frame_number, magic_code, frames_state, masks
489
 
490
  if frame_idx not in frames_state:
491
  logger.error(f"Frame {frame_idx} not in state")
492
- return None, masks_state
493
 
494
  try:
495
  # Get the original frame
@@ -518,7 +522,8 @@ def handle_image_click(segment_id, frame_number, magic_code, frames_state, masks
518
 
519
  logger.info(f"Successfully updated mask for frame {frame_idx}")
520
 
521
- return result_image, masks_state
 
522
 
523
  except Exception as e:
524
  logger.error(f"Error calling SAM-3 function: {e}")
@@ -526,45 +531,58 @@ def handle_image_click(segment_id, frame_number, magic_code, frames_state, masks
526
  frame = frames_state[frame_idx]
527
  mask = masks_state.get(frame_idx, None)
528
  result_image = composite_image_with_mask(frame, mask, True)
529
- return result_image, masks_state
530
 
531
  except Exception as e:
532
  logger.error(f"Error handling image click: {e}")
533
- return None, masks_state
534
 
535
 
536
  def download_segment(segment_id, frames_state, masks_state):
537
  """
538
- Package and download the edited segment (frames + masks) as a ZIP file.
 
 
 
 
539
  """
540
- if not segment_id or not frames_state:
541
- return "No segment selected"
 
 
 
 
542
 
543
  logger.info(f"Download requested for segment: {segment_id}")
544
 
545
  try:
546
- # Create temporary directory for files
 
 
547
  with tempfile.TemporaryDirectory() as tmpdir:
548
- # Save all frames and masks
549
- for frame_idx, frame_img in frames_state.items():
550
- frame_path = os.path.join(tmpdir, f"frame_{frame_idx:06d}.png")
551
- frame_img.save(frame_path)
552
-
553
- if frame_idx in masks_state:
554
- mask_path = os.path.join(tmpdir, f"alpha_frame_{frame_idx:06d}.png")
555
- masks_state[frame_idx].save(mask_path)
556
-
557
- # Create ZIP
558
- import shutil
559
- zip_path = f"/tmp/segment_{segment_id}.zip"
560
  shutil.make_archive(zip_path.replace('.zip', ''), 'zip', tmpdir)
561
 
562
- logger.info(f"Created ZIP at {zip_path}")
563
- return zip_path
 
 
 
 
564
 
565
  except Exception as e:
566
  logger.error(f"Error creating download package: {e}")
567
- return f"Error: {str(e)}"
 
 
 
 
568
 
569
 
570
  # Create a professional Gradio interface using the Golden ratio (1.618) for proportions
@@ -753,6 +771,53 @@ label {
753
  padding: 15px;
754
  }
755
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  """
757
 
758
  # Create a Blocks interface for more customization
@@ -854,11 +919,69 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue
854
  # Row 6: Download Segment button
855
  with gr.Row(visible=False) as seg_row6:
856
  seg_download_btn = gr.Button("Download Segment", variant="secondary")
 
857
  seg_download_file = gr.File(label="Download", visible=False)
858
 
859
  # Hidden component for keyboard event capture
860
  seg_keyboard_input = gr.Textbox(visible=False, elem_id="seg_keyboard_input")
861
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
862
  # Wire Content Moderation processing
863
  cm_process_btn.click(
864
  fn=process_video,
@@ -877,7 +1000,7 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue
877
  seg_id_dropdown.change(
878
  fn=handle_segment_selection,
879
  inputs=[seg_id_dropdown, seg_magic_code],
880
- outputs=[seg_image_editor, seg_frame_slider, frames_state, masks_state, magic_code_state]
881
  )
882
 
883
  # Frame slider handler
@@ -898,14 +1021,14 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue
898
  seg_image_editor.select(
899
  fn=handle_image_click,
900
  inputs=[seg_id_dropdown, seg_frame_slider, magic_code_state, frames_state, masks_state],
901
- outputs=[seg_image_editor, masks_state]
902
  )
903
 
904
  # Download button handler
905
  seg_download_btn.click(
906
  fn=download_segment,
907
  inputs=[seg_id_dropdown, frames_state, masks_state],
908
- outputs=[seg_download_file]
909
  )
910
 
911
  # Keyboard navigation handler
 
445
  Handle segment selection: download all files and initialize the view.
446
  """
447
  if not segment_id or not magic_code:
448
+ return None, gr.update(maximum=0, value=0), {}, {}, magic_code, gr.update(interactive=False)
449
 
450
  logger.info(f"Segment selected: {segment_id}")
451
 
 
454
 
455
  if not frames_dict:
456
  logger.error("No frames downloaded")
457
+ return None, gr.update(maximum=0, value=0), {}, {}, magic_code, gr.update(interactive=False)
458
 
459
  # Load first frame
460
  frame_0 = frames_dict.get(0, None)
 
465
  # Initial display without mask
466
  result_image = composite_image_with_mask(frame_0, masks_dict.get(0, None), False)
467
 
468
+ # Enable download button only if masks are available
469
+ has_masks = len(masks_dict) > 0
470
+
471
  return (
472
  result_image,
473
  gr.update(minimum=0, maximum=max_frame, value=0),
474
  frames_dict,
475
  masks_dict,
476
+ magic_code,
477
+ gr.update(interactive=has_masks)
478
  )
479
 
480
 
 
483
  Handle click on ImageEditor: send coordinates to SAM-3, get new mask, update display.
484
  """
485
  if not segment_id or frames_state is None or evt is None:
486
+ return None, masks_state, gr.update()
487
 
488
  # Extract click coordinates
489
  x, y = evt.index[0], evt.index[1]
 
493
 
494
  if frame_idx not in frames_state:
495
  logger.error(f"Frame {frame_idx} not in state")
496
+ return None, masks_state, gr.update()
497
 
498
  try:
499
  # Get the original frame
 
522
 
523
  logger.info(f"Successfully updated mask for frame {frame_idx}")
524
 
525
+ # Enable download button since we now have at least one mask
526
+ return result_image, masks_state, gr.update(interactive=True)
527
 
528
  except Exception as e:
529
  logger.error(f"Error calling SAM-3 function: {e}")
 
531
  frame = frames_state[frame_idx]
532
  mask = masks_state.get(frame_idx, None)
533
  result_image = composite_image_with_mask(frame, mask, True)
534
+ return result_image, masks_state, gr.update()
535
 
536
  except Exception as e:
537
  logger.error(f"Error handling image click: {e}")
538
+ return None, masks_state, gr.update()
539
 
540
 
541
  def download_segment(segment_id, frames_state, masks_state):
542
  """
543
+ Package and download only the alpha masks for the selected segment as a ZIP file.
544
+ ZIP filename will be {segment_id}.zip.
545
+
546
+ Returns:
547
+ Tuple of (status_message, file_path, status_visibility)
548
  """
549
+ if not segment_id:
550
+ return gr.update(value="No segment selected", visible=True), None, gr.update(visible=True)
551
+
552
+ if not masks_state:
553
+ logger.warning(f"No alpha masks available for segment: {segment_id}")
554
+ return gr.update(value="No alpha masks available", visible=True), None, gr.update(visible=True)
555
 
556
  logger.info(f"Download requested for segment: {segment_id}")
557
 
558
  try:
559
+ import shutil
560
+
561
+ # Create temporary directory for alpha mask files only
562
  with tempfile.TemporaryDirectory() as tmpdir:
563
+ # Save only alpha masks
564
+ for frame_idx, mask_img in masks_state.items():
565
+ mask_path = os.path.join(tmpdir, f"alpha_frame_{frame_idx:06d}.png")
566
+ mask_img.save(mask_path)
567
+
568
+ # Create ZIP with segment UUID as filename
569
+ zip_path = f"/tmp/{segment_id}.zip"
 
 
 
 
 
570
  shutil.make_archive(zip_path.replace('.zip', ''), 'zip', tmpdir)
571
 
572
+ logger.info(f"Created ZIP at {zip_path} with {len(masks_state)} alpha masks")
573
+ return (
574
+ gr.update(value=f"✓ Downloaded {len(masks_state)} alpha masks", visible=True),
575
+ zip_path,
576
+ gr.update(visible=True)
577
+ )
578
 
579
  except Exception as e:
580
  logger.error(f"Error creating download package: {e}")
581
+ return (
582
+ gr.update(value=f"Error: {str(e)}", visible=True),
583
+ None,
584
+ gr.update(visible=True)
585
+ )
586
 
587
 
588
  # Create a professional Gradio interface using the Golden ratio (1.618) for proportions
 
771
  padding: 15px;
772
  }
773
  }
774
+
775
+ /* Loading modal overlay */
776
+ #download-loading-modal {
777
+ display: none;
778
+ position: fixed;
779
+ top: 0;
780
+ left: 0;
781
+ width: 100%;
782
+ height: 100%;
783
+ background-color: rgba(0, 0, 0, 0.7);
784
+ z-index: 9999;
785
+ justify-content: center;
786
+ align-items: center;
787
+ }
788
+
789
+ #download-loading-modal.show {
790
+ display: flex;
791
+ }
792
+
793
+ .loading-content {
794
+ background-color: var(--card-bg);
795
+ padding: 40px;
796
+ border-radius: var(--border-radius);
797
+ text-align: center;
798
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
799
+ }
800
+
801
+ .spinner {
802
+ border: 4px solid var(--border-color);
803
+ border-top: 4px solid var(--primary-color);
804
+ border-radius: 50%;
805
+ width: 50px;
806
+ height: 50px;
807
+ animation: spin 1s linear infinite;
808
+ margin: 0 auto 20px;
809
+ }
810
+
811
+ @keyframes spin {
812
+ 0% { transform: rotate(0deg); }
813
+ 100% { transform: rotate(360deg); }
814
+ }
815
+
816
+ .loading-text {
817
+ color: var(--text-color);
818
+ font-size: 18px;
819
+ font-weight: 500;
820
+ }
821
  """
822
 
823
  # Create a Blocks interface for more customization
 
919
  # Row 6: Download Segment button
920
  with gr.Row(visible=False) as seg_row6:
921
  seg_download_btn = gr.Button("Download Segment", variant="secondary")
922
+ seg_download_status = gr.Textbox(label="Status", value="", visible=False, interactive=False)
923
  seg_download_file = gr.File(label="Download", visible=False)
924
 
925
  # Hidden component for keyboard event capture
926
  seg_keyboard_input = gr.Textbox(visible=False, elem_id="seg_keyboard_input")
927
 
928
+ # Loading modal HTML
929
+ gr.HTML("""
930
+ <div id="download-loading-modal">
931
+ <div class="loading-content">
932
+ <div class="spinner"></div>
933
+ <div class="loading-text">Preparing download…</div>
934
+ </div>
935
+ </div>
936
+ <script>
937
+ function showDownloadLoading() {
938
+ const modal = document.getElementById('download-loading-modal');
939
+ if (modal) modal.classList.add('show');
940
+ }
941
+
942
+ function hideDownloadLoading() {
943
+ const modal = document.getElementById('download-loading-modal');
944
+ if (modal) modal.classList.remove('show');
945
+ }
946
+
947
+ // Listen for download button clicks
948
+ document.addEventListener('DOMContentLoaded', function() {
949
+ const checkButton = setInterval(function() {
950
+ const downloadBtn = document.querySelector('button:has-text("Download Segment")');
951
+ if (!downloadBtn) {
952
+ // Fallback: find button by content
953
+ const buttons = document.querySelectorAll('button');
954
+ for (let btn of buttons) {
955
+ if (btn.textContent.includes('Download Segment')) {
956
+ setupDownloadButton(btn);
957
+ clearInterval(checkButton);
958
+ break;
959
+ }
960
+ }
961
+ } else {
962
+ setupDownloadButton(downloadBtn);
963
+ clearInterval(checkButton);
964
+ }
965
+ }, 500);
966
+
967
+ function setupDownloadButton(btn) {
968
+ btn.addEventListener('click', function() {
969
+ showDownloadLoading();
970
+ // Hide modal after function completes (watch for Gradio loading to finish)
971
+ setTimeout(function checkLoading() {
972
+ const gradioLoading = document.querySelector('.loading');
973
+ if (!gradioLoading) {
974
+ setTimeout(hideDownloadLoading, 500);
975
+ } else {
976
+ setTimeout(checkLoading, 200);
977
+ }
978
+ }, 200);
979
+ });
980
+ }
981
+ });
982
+ </script>
983
+ """)
984
+
985
  # Wire Content Moderation processing
986
  cm_process_btn.click(
987
  fn=process_video,
 
1000
  seg_id_dropdown.change(
1001
  fn=handle_segment_selection,
1002
  inputs=[seg_id_dropdown, seg_magic_code],
1003
+ outputs=[seg_image_editor, seg_frame_slider, frames_state, masks_state, magic_code_state, seg_download_btn]
1004
  )
1005
 
1006
  # Frame slider handler
 
1021
  seg_image_editor.select(
1022
  fn=handle_image_click,
1023
  inputs=[seg_id_dropdown, seg_frame_slider, magic_code_state, frames_state, masks_state],
1024
+ outputs=[seg_image_editor, masks_state, seg_download_btn]
1025
  )
1026
 
1027
  # Download button handler
1028
  seg_download_btn.click(
1029
  fn=download_segment,
1030
  inputs=[seg_id_dropdown, frames_state, masks_state],
1031
+ outputs=[seg_download_status, seg_download_file, seg_download_status]
1032
  )
1033
 
1034
  # Keyboard navigation handler