Spaces:
Sleeping
Sleeping
Add arrow key navigation for Segmentation Editing frame slider
Browse files- Implement handle_keyboard_navigation() callback function
- Add JavaScript keydown listener scoped to Segmentation tab
- Clamp frame values to min/max boundaries from frames_state
- Reuse existing composite_image_with_mask logic for consistency
- Prevent default browser behavior for captured arrow keys
- Update both ImageEditor and slider value synchronously
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
app.py
CHANGED
|
@@ -378,6 +378,68 @@ def load_segment_frame(segment_id, frame_number, show_mask, magic_code_state, fr
|
|
| 378 |
return result_image, gr.update()
|
| 379 |
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
def handle_segment_selection(segment_id, magic_code):
|
| 382 |
"""
|
| 383 |
Handle segment selection: download all files and initialize the view.
|
|
@@ -794,6 +856,9 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue
|
|
| 794 |
seg_download_btn = gr.Button("Download Segment", variant="secondary")
|
| 795 |
seg_download_file = gr.File(label="Download", visible=False)
|
| 796 |
|
|
|
|
|
|
|
|
|
|
| 797 |
# Wire Content Moderation processing
|
| 798 |
cm_process_btn.click(
|
| 799 |
fn=process_video,
|
|
@@ -843,6 +908,61 @@ with gr.Blocks(css=css, theme=gr.themes.Soft(primary_hue="indigo", secondary_hue
|
|
| 843 |
outputs=[seg_download_file]
|
| 844 |
)
|
| 845 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 846 |
if __name__ == "__main__":
|
| 847 |
# To run this file locally, you'll need to install gradio and requests:
|
| 848 |
# pip install gradio requests
|
|
|
|
| 378 |
return result_image, gr.update()
|
| 379 |
|
| 380 |
|
| 381 |
+
def handle_keyboard_navigation(key_code, segment_id, current_frame, show_mask, magic_code_state, frames_state, masks_state):
|
| 382 |
+
"""
|
| 383 |
+
Handle left/right arrow key navigation for frame slider.
|
| 384 |
+
|
| 385 |
+
Args:
|
| 386 |
+
key_code: JavaScript key code ('ArrowLeft' or 'ArrowRight')
|
| 387 |
+
segment_id: Current segment ID
|
| 388 |
+
current_frame: Current frame number
|
| 389 |
+
show_mask: Whether to show alpha mask overlay
|
| 390 |
+
magic_code_state: Magic code state
|
| 391 |
+
frames_state: Frames dictionary state
|
| 392 |
+
masks_state: Masks dictionary state
|
| 393 |
+
|
| 394 |
+
Returns:
|
| 395 |
+
Tuple of (updated image, updated slider value)
|
| 396 |
+
"""
|
| 397 |
+
if not segment_id or frames_state is None:
|
| 398 |
+
return None, gr.update()
|
| 399 |
+
|
| 400 |
+
frames_dict = frames_state
|
| 401 |
+
masks_dict = masks_state
|
| 402 |
+
|
| 403 |
+
# Get min/max from available frames
|
| 404 |
+
available_frames = sorted(frames_dict.keys())
|
| 405 |
+
if not available_frames:
|
| 406 |
+
return None, gr.update()
|
| 407 |
+
|
| 408 |
+
min_frame = available_frames[0]
|
| 409 |
+
max_frame = available_frames[-1]
|
| 410 |
+
|
| 411 |
+
# Calculate new frame number
|
| 412 |
+
new_frame = int(current_frame)
|
| 413 |
+
|
| 414 |
+
if key_code == 'ArrowLeft':
|
| 415 |
+
new_frame = max(min_frame, new_frame - 1)
|
| 416 |
+
elif key_code == 'ArrowRight':
|
| 417 |
+
new_frame = min(max_frame, new_frame + 1)
|
| 418 |
+
else:
|
| 419 |
+
# Unknown key, no change
|
| 420 |
+
return None, gr.update()
|
| 421 |
+
|
| 422 |
+
# If frame didn't change (at boundary), return early
|
| 423 |
+
if new_frame == int(current_frame):
|
| 424 |
+
return None, gr.update()
|
| 425 |
+
|
| 426 |
+
logger.info(f"Keyboard navigation: {key_code} -> frame {new_frame}")
|
| 427 |
+
|
| 428 |
+
# Load the new frame using existing logic
|
| 429 |
+
if new_frame not in frames_dict:
|
| 430 |
+
logger.warning(f"Frame {new_frame} not found in downloaded frames")
|
| 431 |
+
return None, gr.update()
|
| 432 |
+
|
| 433 |
+
frame = frames_dict[new_frame]
|
| 434 |
+
mask = masks_dict.get(new_frame, None)
|
| 435 |
+
|
| 436 |
+
# Composite image with mask
|
| 437 |
+
result_image = composite_image_with_mask(frame, mask, show_mask)
|
| 438 |
+
|
| 439 |
+
# Return updated image and new slider value
|
| 440 |
+
return result_image, gr.update(value=new_frame)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
def handle_segment_selection(segment_id, magic_code):
|
| 444 |
"""
|
| 445 |
Handle segment selection: download all files and initialize the view.
|
|
|
|
| 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,
|
|
|
|
| 908 |
outputs=[seg_download_file]
|
| 909 |
)
|
| 910 |
|
| 911 |
+
# Keyboard navigation handler
|
| 912 |
+
seg_keyboard_input.change(
|
| 913 |
+
fn=handle_keyboard_navigation,
|
| 914 |
+
inputs=[
|
| 915 |
+
seg_keyboard_input,
|
| 916 |
+
seg_id_dropdown,
|
| 917 |
+
seg_frame_slider,
|
| 918 |
+
seg_show_mask,
|
| 919 |
+
magic_code_state,
|
| 920 |
+
frames_state,
|
| 921 |
+
masks_state
|
| 922 |
+
],
|
| 923 |
+
outputs=[seg_image_editor, seg_frame_slider]
|
| 924 |
+
)
|
| 925 |
+
|
| 926 |
+
# Add JavaScript to capture arrow key events
|
| 927 |
+
demo.load(
|
| 928 |
+
None,
|
| 929 |
+
None,
|
| 930 |
+
None,
|
| 931 |
+
js="""
|
| 932 |
+
() => {
|
| 933 |
+
// Wait for the DOM to be ready
|
| 934 |
+
setTimeout(() => {
|
| 935 |
+
const keyboardInput = document.getElementById('seg_keyboard_input');
|
| 936 |
+
if (!keyboardInput) {
|
| 937 |
+
console.warn('Keyboard input element not found');
|
| 938 |
+
return;
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
// Add keydown listener to document
|
| 942 |
+
document.addEventListener('keydown', (e) => {
|
| 943 |
+
// Only handle arrow keys
|
| 944 |
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
| 945 |
+
// Check if we're in the Segmentation Editing tab
|
| 946 |
+
const segTab = document.querySelector('[id*="segmentation-editing"]');
|
| 947 |
+
const activeTab = document.querySelector('.tab-nav button.selected');
|
| 948 |
+
|
| 949 |
+
if (activeTab && activeTab.textContent.includes('Segmentation Editing')) {
|
| 950 |
+
e.preventDefault();
|
| 951 |
+
|
| 952 |
+
// Update the hidden input to trigger the change event
|
| 953 |
+
const textarea = keyboardInput.querySelector('textarea');
|
| 954 |
+
if (textarea) {
|
| 955 |
+
textarea.value = e.key;
|
| 956 |
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
| 957 |
+
}
|
| 958 |
+
}
|
| 959 |
+
}
|
| 960 |
+
});
|
| 961 |
+
}, 1000);
|
| 962 |
+
}
|
| 963 |
+
"""
|
| 964 |
+
)
|
| 965 |
+
|
| 966 |
if __name__ == "__main__":
|
| 967 |
# To run this file locally, you'll need to install gradio and requests:
|
| 968 |
# pip install gradio requests
|