from pathlib import Path import gradio as gr from gesture_logic import FIRST, LAST, NEXT, PREV, NONE, infer_gesture_command from hand_tracker import detect_hand from state import ImageState ASSETS_DIR = Path("assets") APP_CSS = """ #app-shell { max-width: 1280px; margin: 0 auto; } .app-heading h1 { font-size: 26px; line-height: 1.2; margin-bottom: 4px; } .app-heading p { color: #4b5563; margin-top: 0; } .preview-card { border: 1px solid #e5e7eb; border-radius: 8px; background: #ffffff; padding: 12px; } .preview-title { font-weight: 700; margin: 0 0 8px; } .preview-image { height: min(46vw, 430px); min-height: 280px; overflow: hidden; } .preview-image img, .preview-image video { width: 100% !important; height: 100% !important; object-fit: contain !important; } .preview-image .icon-wrap, .preview-image .empty, .preview-image .label-wrap { display: none !important; } #status-box textarea { font-size: 16px; font-weight: 600; } .command-row button { min-height: 44px; } @media (max-width: 900px) { .preview-image { height: 320px; } } """ def build_status(command: str, index: int, total: int) -> str: if total == 0: return f"Command: {command} | Index: 0/0 (no images in assets/)" return f"Command: {command} | Index: {index + 1}/{total}" def apply_command(command: str, current_index: int, webcam_frame=None): image_state = ImageState(str(ASSETS_DIR)) total = len(image_state.image_paths) if total > 0: if current_index is None: image_state.index = 0 else: image_state.index = max(0, min(int(current_index), total - 1)) # Placeholder call to keep the future hand-tracking interface in place. _ = detect_hand(webcam_frame) if command == NEXT: image_state.next_image() elif command == PREV: image_state.prev_image() elif command == FIRST: image_state.first_image() elif command == LAST: image_state.last_image() current_image = image_state.get_current_image() total = len(image_state.image_paths) status = build_status(command, image_state.index, total) return current_image, status, image_state.index def on_prev(current_index: int): return apply_command(PREV, current_index, None) def on_next(current_index: int): return apply_command(NEXT, current_index, None) def on_first(current_index: int): return apply_command(FIRST, current_index, None) def on_last(current_index: int): return apply_command(LAST, current_index, None) def on_webcam_frame(webcam_frame, current_index: int): hand_data = detect_hand(webcam_frame) command = infer_gesture_command(hand_data) return apply_command(command, current_index, webcam_frame) def build_demo(): initial_state = ImageState(str(ASSETS_DIR)) initial_image = initial_state.get_current_image() initial_total = len(initial_state.image_paths) initial_status = build_status(NONE, initial_state.index, initial_total) with gr.Blocks(title="手勢影像播放器", elem_id="app-shell") as demo: gr.Markdown( "# 手勢影像播放器\n使用按鈕或單手手勢切換圖片。", elem_classes="app-heading", ) with gr.Row(): with gr.Column(scale=1, elem_classes="preview-card"): gr.Markdown("網路攝影機", elem_classes="preview-title") webcam_input = gr.Image( sources=["webcam"], type="numpy", streaming=True, show_label=False, height=420, elem_classes="preview-image", placeholder="", ) with gr.Column(scale=1, elem_classes="preview-card"): gr.Markdown("目前圖片", elem_classes="preview-title") current_image = gr.Image( value=initial_image, type="numpy", show_label=False, height=420, elem_classes="preview-image", buttons=[], placeholder="", ) status_text = gr.Textbox( value=initial_status, label="狀態", interactive=False, elem_id="status-box", ) state_holder = gr.State(initial_state.index) with gr.Row(elem_classes="command-row"): btn_prev = gr.Button("PREV") btn_next = gr.Button("NEXT") btn_first = gr.Button("FIRST") btn_last = gr.Button("LAST") btn_prev.click( fn=on_prev, inputs=[state_holder], outputs=[current_image, status_text, state_holder], ) btn_next.click( fn=on_next, inputs=[state_holder], outputs=[current_image, status_text, state_holder], ) btn_first.click( fn=on_first, inputs=[state_holder], outputs=[current_image, status_text, state_holder], ) btn_last.click( fn=on_last, inputs=[state_holder], outputs=[current_image, status_text, state_holder], ) webcam_input.stream( fn=on_webcam_frame, inputs=[webcam_input, state_holder], outputs=[current_image, status_text, state_holder], ) webcam_input.change( fn=on_webcam_frame, inputs=[webcam_input, state_holder], outputs=[current_image, status_text, state_holder], ) return demo if __name__ == "__main__": app = build_demo() app.launch(show_error=True, css=APP_CSS)