# 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( """