Spaces:
Runtime error
Runtime error
| # 1. Import all the libraries | |
| import gradio as gr | |
| import cv2 | |
| import mediapipe as mp | |
| import numpy as np | |
| import math | |
| import os # Import os to check for file | |
| # --- This dictionary now points to YOUR uploaded presets --- | |
| PRELOADED_MODELS = { | |
| "Preset 1": "preset_1.stl", | |
| "Preset 2": "preset_2.stl", | |
| "Preset 3": "preset_3.stl" | |
| } | |
| # 4. Set up our MediaPipe objects | |
| mp_drawing = mp.solutions.drawing_utils | |
| mp_hands = mp.solutions.hands | |
| # We are detecting ONE hand | |
| hands = mp_hands.Hands( | |
| max_num_hands=1, | |
| min_detection_confidence=0.7, | |
| min_tracking_confidence=0.5 | |
| ) | |
| # 5. --- GESTURE RECOGNIZER CLASS (Same as before) --- | |
| class GestureRecognizer: | |
| def __init__(self): | |
| self.PINCH_THRESHOLD = 0.08 | |
| def _calculate_distance(self, lm1, lm2): | |
| return math.sqrt((lm1.x - lm2.x)**2 + (lm1.y - lm2.y)**2) | |
| def recognize(self, hand_landmarks): | |
| landmarks = hand_landmarks.landmark | |
| wrist_pos = (landmarks[mp_hands.HandLandmark.WRIST].x, landmarks[mp_hands.HandLandmark.WRIST].y) | |
| # --- PRE-CALCULATE FINGER STATES --- | |
| # Note: In screen coordinates, Y increases downwards. | |
| # TIP < PIP means the finger is pointing UP (Extended) | |
| # TIP > PIP means the finger is curled DOWN (Curled) | |
| index_tip_y = landmarks[mp_hands.HandLandmark.INDEX_FINGER_TIP].y | |
| index_pip_y = landmarks[mp_hands.HandLandmark.INDEX_FINGER_PIP].y | |
| middle_tip_y = landmarks[mp_hands.HandLandmark.MIDDLE_FINGER_TIP].y | |
| middle_pip_y = landmarks[mp_hands.HandLandmark.MIDDLE_FINGER_PIP].y | |
| ring_tip_y = landmarks[mp_hands.HandLandmark.RING_FINGER_TIP].y | |
| ring_pip_y = landmarks[mp_hands.HandLandmark.RING_FINGER_PIP].y | |
| pinky_tip_y = landmarks[mp_hands.HandLandmark.PINKY_TIP].y | |
| pinky_pip_y = landmarks[mp_hands.HandLandmark.PINKY_PIP].y | |
| # Determine states | |
| index_extended = index_tip_y < index_pip_y | |
| middle_extended = middle_tip_y < middle_pip_y | |
| ring_curled = ring_tip_y > ring_pip_y | |
| pinky_curled = pinky_tip_y > pinky_pip_y | |
| # 1. Check for "PEACE SIGN" | |
| # Index UP, Middle UP, Ring DOWN, Pinky DOWN | |
| if index_extended and middle_extended and ring_curled and pinky_curled: | |
| return "**PEACE**", wrist_pos | |
| # 2. Check for "FIST" | |
| # Index DOWN, Middle DOWN, Ring DOWN, Pinky DOWN | |
| # (Your previous code checked if Index was Extended) | |
| index_curled = not index_extended # TIP > PIP | |
| middle_curled = not middle_extended # TIP > PIP | |
| if index_curled and middle_curled and ring_curled and pinky_curled: | |
| return "**FIST**", wrist_pos # Return wrist position | |
| # 3. Check for "PINCH" | |
| thumb_tip = landmarks[mp_hands.HandLandmark.THUMB_TIP] | |
| index_tip = landmarks[mp_hands.HandLandmark.INDEX_FINGER_TIP] | |
| pinch_distance = self._calculate_distance(thumb_tip, index_tip) | |
| if pinch_distance < self.PINCH_THRESHOLD: | |
| return "**PINCH**", wrist_pos | |
| # 4. If no other gesture, it's "OPEN" | |
| return "**OPEN**", None | |
| # 6. --- Create an instance of our recognizer --- | |
| recognizer = GestureRecognizer() | |
| # 7. --- GESTURE PROCESSING FUNCTION (OPTIMIZED) --- | |
| DEFAULT_CAMERA = (0, 90, 150) | |
| ROTATION_SENSITIVITY = 360 | |
| ZOOM_SENSITIVITY = 75 | |
| def process_image(webcam_frame, camera_state, prev_hand_pos_state): | |
| gesture_text = "No hand detected" | |
| new_camera_state = camera_state | |
| new_prev_hand_pos = None | |
| (old_azimuth, old_polar, old_zoom) = camera_state | |
| if webcam_frame is None: | |
| return "Waiting...", DEFAULT_CAMERA, None, gr.update(camera_position=DEFAULT_CAMERA) | |
| image = webcam_frame | |
| image.flags.writeable = False | |
| results = hands.process(image) | |
| if results.multi_hand_landmarks: | |
| hand_landmarks = results.multi_hand_landmarks[0] | |
| current_gesture, current_hand_pos = recognizer.recognize(hand_landmarks) | |
| gesture_text = current_gesture.replace("**", "") | |
| if current_gesture == "**PEACE**": | |
| new_camera_state = DEFAULT_CAMERA | |
| gesture_text = "Resetting view!" | |
| elif current_hand_pos and prev_hand_pos_state: | |
| delta_x = current_hand_pos[0] - prev_hand_pos_state[0] | |
| delta_y = current_hand_pos[1] - prev_hand_pos_state[1] | |
| if current_gesture == "**PINCH**": | |
| new_azimuth = old_azimuth + (delta_x * ROTATION_SENSITIVITY) | |
| new_polar = old_polar - (delta_y * ROTATION_SENSITIVITY) | |
| new_camera_state = (new_azimuth, new_polar, old_zoom) | |
| gesture_text = "Rotating..." | |
| elif current_gesture == "**FIST**": | |
| new_zoom = old_zoom + (delta_x * ZOOM_SENSITIVITY) | |
| new_zoom = max(0.5, min(new_zoom, 400.0)) | |
| new_camera_state = (old_azimuth, old_polar, new_zoom) | |
| gesture_text = "Zooming..." | |
| if current_hand_pos: | |
| new_prev_hand_pos = current_hand_pos | |
| else: | |
| new_prev_hand_pos = None | |
| # Return 4 values | |
| return gesture_text, new_camera_state, new_prev_hand_pos, gr.update(camera_position=new_camera_state) | |
| # 8. --- HELPER FUNCTIONS --- | |
| def load_preloaded_model(model_name): | |
| file_path = PRELOADED_MODELS[model_name] | |
| return gr.update(value=file_path), gr.update(value=DEFAULT_CAMERA) | |
| def load_uploaded_model(temp_file): | |
| return gr.update(value=temp_file.name), gr.update(value=DEFAULT_CAMERA) | |
| # --- 9. NEW: LOGIN FUNCTION --- | |
| def login_function(password): | |
| # --- THIS IS THE FIX --- | |
| # It now reads the SECURE password from the environment | |
| correct_password = os.environ.get("APP_PASSWORD") | |
| if password == correct_password: | |
| # Return updates to hide login and show app | |
| return gr.update(visible=False), gr.update(visible=True), gr.update(value="") | |
| else: | |
| # Return updates to show error | |
| return gr.update(visible=True), gr.update(visible=False), gr.update(value="Incorrect Password") | |
| # --- 10. Create and launch the Gradio Interface --- | |
| with gr.Blocks(theme=gr.themes.Glass()) as demo: | |
| # --- This row is the LOGIN SCREEN (visible by default) --- | |
| with gr.Row(visible=True) as login_page: | |
| with gr.Column(scale=1): | |
| pass # Empty spacer | |
| with gr.Column(scale=2): | |
| gr.Markdown("# ๐๏ธ Gesture-Controlled 3D Viewer") | |
| gr.Markdown("Please enter the password to continue.") | |
| password_input = gr.Textbox(label="Password", type="password") | |
| login_button = gr.Button("Login") | |
| error_output = gr.Textbox(label="Error", interactive=False) | |
| with gr.Column(scale=1): | |
| pass # Empty spacer | |
| # --- This column is the MAIN APP (hidden by default) --- | |
| with gr.Column(visible=False) as main_app: | |
| camera_state = gr.State(value=DEFAULT_CAMERA) | |
| prev_hand_pos_state = gr.State(value=None) | |
| with gr.Row(): | |
| gr.Markdown("# ๐๏ธ Gesture-Controlled 3D Viewer") | |
| with gr.Row(): | |
| # --- UI Column for controls --- | |
| with gr.Column(scale=1): | |
| gr.Markdown("### 1. Select a Model") | |
| with gr.Tabs(): | |
| with gr.Tab("Your Presets"): | |
| radio_picker = gr.Radio( | |
| choices=list(PRELOADED_MODELS.keys()), | |
| label="Select your preset model", | |
| value="Preset 1" | |
| ) | |
| with gr.Tab("Upload"): | |
| file_uploader = gr.File( | |
| label="Upload .stl, .obj, .glb, or .3mf", | |
| file_types=['.stl', '.obj', '.glb', '.gltf', '.3mf'] | |
| ) | |
| gr.Markdown("### 2. Control with Webcam") | |
| webcam_input = gr.Image(sources=["webcam"], streaming=True, label="Webcam Feed") | |
| gr.Markdown("### 3. Gesture Status") | |
| text_output = gr.Text(label="Status") | |
| # --- MAIN COLUMN for 3D viewer --- | |
| with gr.Column(scale=3): | |
| default_model_path = PRELOADED_MODELS.get("Preset 1", None) | |
| if not os.path.exists(default_model_path): | |
| print(f"Warning: Default preset '{default_model_path}' not found.") | |
| default_model_path = None | |
| model_3d = gr.Model3D( | |
| value=default_model_path, | |
| label=None, | |
| camera_position=DEFAULT_CAMERA, | |
| interactive=False | |
| ) | |
| gr.Markdown( | |
| """ | |
| <div style="width: 100%; text-align: center;"> | |
| <b>Gesture Controls:</b> โ๏ธ <b>Peace Sign:</b> Reset/Home | ๐ <b>Pinch & Move:</b> Rotate | โ <b>Fist & Move Left/Right:</b> Zoom In / Zoom Out | |
| </div> | |
| """ | |
| ) | |
| # --- 11. WIRE UP ALL CONTROLS --- | |
| # 1. Wire up the Login Button | |
| login_button.click( | |
| fn=login_function, | |
| inputs=password_input, | |
| outputs=[login_page, main_app, error_output] | |
| ) | |
| # 2. Wire up the Radio buttons | |
| radio_picker.change( | |
| fn=load_preloaded_model, | |
| inputs=radio_picker, | |
| outputs=[model_3d, camera_state] | |
| ) | |
| # 3. Wire up the File uploader | |
| file_uploader.upload( | |
| fn=load_uploaded_model, | |
| inputs=file_uploader, | |
| outputs=[model_3d, camera_state] | |
| ) | |
| # 4. Wire up the webcam gesture controls | |
| webcam_input.stream( | |
| fn=process_image, | |
| inputs=[ | |
| webcam_input, | |
| camera_state, | |
| prev_hand_pos_state | |
| ], | |
| outputs=[ | |
| text_output, | |
| camera_state, | |
| prev_hand_pos_state, | |
| model_3d | |
| ] | |
| ) | |
| # --- | |
| # THIS IS THE LAUNCH COMMAND FOR HUGGING FACE | |
| # --- | |
| # We use share=True to fix the ValueError | |
| demo.launch(share=True) |