import os import json import random from flask import Flask, render_template, request, jsonify, send_from_directory app = Flask(__name__, static_folder='static', template_folder='templates') # Dictionary to track which puzzles have been shown for each CAPTCHA type seen_puzzles = {} # List to track recently used CAPTCHA types to avoid repetition recent_types = [] # How many types to remember before allowing repetition MAX_RECENT_TYPES = 5 PUZZLE_TYPE_SEQUENCE = [ 'Dice_Count', 'Geometry_Click', 'Rotation_Match', 'Slide_Puzzle', 'Unusual_Detection', 'Image_Recognition', 'Bingo', 'Image_Matching', 'Patch_Select', 'Dart_Count', 'Object_Match', 'Select_Animal', 'Coordinates', 'Path_Finder', 'Place_Dot', 'Connect_icon', 'Click_Order', 'Hold_Button', 'Misleading_Click', 'Pick_Area' ] sequential_index = 0 # Load ground truth data for a specific type def load_ground_truth(captcha_type): path = os.path.join('captcha_data', captcha_type, 'ground_truth.json') try: with open(path, 'r') as f: return json.load(f) except (FileNotFoundError, json.JSONDecodeError): return {} # Get available CAPTCHA types def get_captcha_types(): base_dir = 'captcha_data' if not os.path.exists(base_dir): return [] return [d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d))] @app.route('/') def index(): return render_template('index.html') @app.route('/captcha_data//') def serve_captcha(captcha_type, filename): return send_from_directory(os.path.join('captcha_data', captcha_type), filename) @app.route('/captcha_data///') def serve_captcha_subdir(captcha_type, subdir, filename): return send_from_directory(os.path.join('captcha_data', captcha_type, subdir), filename) @app.route('/api/get_puzzle', methods=['GET']) def get_puzzle(): global recent_types # Check if we should return a random puzzle from any type is_random = request.args.get('random', 'false').lower() == 'true' # Get all available CAPTCHA types captcha_types = get_captcha_types() if not captcha_types: return jsonify({'error': 'No CAPTCHA types found'}), 404 # Check if we're in debug mode for a specific type debug_type = request.args.get('debug_type') mode = request.args.get('mode', '').lower() if debug_type and debug_type in captcha_types: puzzle_type = debug_type elif not is_random and mode == 'sequential': global sequential_index puzzle_type = PUZZLE_TYPE_SEQUENCE[sequential_index % len(PUZZLE_TYPE_SEQUENCE)] sequential_index += 1 elif is_random: # Select a random CAPTCHA type, avoiding recently used types if possible available_types = [t for t in captcha_types if t not in recent_types] # If all types have been used recently, reset the tracking if not available_types: recent_types = [] available_types = captcha_types puzzle_type = random.choice(available_types) # Add to recent types and maintain maximum length recent_types.append(puzzle_type) if len(recent_types) > MAX_RECENT_TYPES: recent_types.pop(0) else: # Get puzzle type from query parameter puzzle_type = request.args.get('type', 'Dice_Count') # Check if puzzle type exists if puzzle_type not in captcha_types: return jsonify({'error': f'Invalid puzzle type: {puzzle_type}'}), 400 # Load ground truth for the selected type ground_truth = load_ground_truth(puzzle_type) if not ground_truth: return jsonify({'error': f'No puzzles found for type: {puzzle_type}'}), 404 puzzle_files = list(ground_truth.keys()) # Select a random puzzle, avoiding repetition if possible if puzzle_type not in seen_puzzles: seen_puzzles[puzzle_type] = set() # Get unseen puzzles unseen_puzzles = [p for p in puzzle_files if p not in seen_puzzles[puzzle_type]] # If all puzzles have been seen, reset the tracking if not unseen_puzzles: seen_puzzles[puzzle_type] = set() unseen_puzzles = puzzle_files # Select a random puzzle from unseen ones selected_puzzle = random.choice(unseen_puzzles) # Mark this puzzle as seen seen_puzzles[puzzle_type].add(selected_puzzle) # Get the appropriate question prompt based on puzzle type if puzzle_type == "Dice_Count": prompt = ground_truth[selected_puzzle].get('prompt', "Sum up the numbers on all the dice") elif puzzle_type == "Geometry_Click": prompt = ground_truth[selected_puzzle].get("question", "Click on the geometric shape") elif puzzle_type == "Rotation_Match": prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to rotate the object to match the reference direction") elif puzzle_type == "Slide_Puzzle": prompt = ground_truth[selected_puzzle].get("prompt", "Drag the slider component to the correct position") elif puzzle_type == "Unusual_Detection": prompt = ground_truth[selected_puzzle].get("prompt", "Select the unusual items in the image") elif puzzle_type == "Image_Recognition": prompt = ground_truth[selected_puzzle].get("prompt", "Select all images matching the description") elif puzzle_type == "Bingo": prompt = ground_truth[selected_puzzle].get("prompt", "Please click two images to exchange their position to line up the same images to a line") elif puzzle_type == "Image_Matching": prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, match the animal in the left and right image.") elif puzzle_type == "Patch_Select": prompt = ground_truth[selected_puzzle].get("prompt", "Select all squares with the specified objects") elif puzzle_type == "Dart_Count": prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to pick the image where all the darts add up to the number in the left image.") elif puzzle_type == "Object_Match": prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to change the number of objects until it matches the left image.") elif puzzle_type == "Select_Animal": prompt = ground_truth[selected_puzzle].get("prompt", "Pick a fox") elif puzzle_type == "Coordinates": prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, move Jerry to the indicated seat") elif puzzle_type == "Path_Finder": prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to move the duck to the spot indicated by the cross") elif puzzle_type == "Place_Dot": prompt = ground_truth[selected_puzzle].get("prompt", "Click to place a Dot at the end of the car's path") elif puzzle_type == "Connect_icon": prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, connect the same two icons with the dotted line as shown on the left.") elif puzzle_type == "Click_Order": prompt = ground_truth[selected_puzzle].get("prompt", "Click the icons in order as shown in the reference image.") elif puzzle_type == "Hold_Button": prompt = ground_truth[selected_puzzle].get("prompt", "Hold the button until it finishes loading.") elif puzzle_type == "Misleading_Click": prompt = ground_truth[selected_puzzle].get("prompt", "Click the image to continue.") elif puzzle_type == "Pick_Area": prompt = ground_truth[selected_puzzle].get("prompt", "Click on the largest area outlined by the dotted line") else: prompt = ground_truth[selected_puzzle].get("prompt", "Solve the CAPTCHA puzzle") # Add input_type to tell the frontend what kind of input to show input_type = "text" if puzzle_type == "Dice_Count": input_type = "number" elif puzzle_type == "Geometry_Click": input_type = "click" elif puzzle_type == "Rotation_Match": input_type = "rotation" elif puzzle_type == "Slide_Puzzle": input_type = "slide" elif puzzle_type == "Unusual_Detection": input_type = "multiselect" elif puzzle_type == "Image_Recognition": input_type = "image_grid" elif puzzle_type == "Bingo": input_type = "bingo_swap" elif puzzle_type == "Image_Matching": input_type = "image_matching" elif puzzle_type == "Patch_Select": input_type = "patch_select" elif puzzle_type == "Dart_Count": input_type = "dart_count" elif puzzle_type == "Object_Match": input_type = "object_match" elif puzzle_type == "Select_Animal": input_type = "select_animal" elif puzzle_type == "Coordinates": input_type = "image_matching" elif puzzle_type == "Path_Finder": input_type = "image_matching" elif puzzle_type == "Place_Dot": input_type = "place_dot" elif puzzle_type == "Connect_icon": input_type = "connect_icon" elif puzzle_type == "Click_Order": input_type = "click_order" elif puzzle_type == "Hold_Button": input_type = "hold_button" elif puzzle_type == "Misleading_Click": input_type = "click" elif puzzle_type == "Pick_Area": input_type = "click" # For Rotation_Match, include additional data needed for the interface additional_data = {} if puzzle_type == "Rotation_Match": # Get reference image and object base name reference_image = ground_truth[selected_puzzle].get("reference_image") object_base_image = ground_truth[selected_puzzle].get("object_base_image") if not reference_image or not object_base_image: # If missing required fields, try another puzzle or fall back return jsonify({'error': f'Invalid rotation puzzle data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' # Get object base name without extension to construct rotated image paths object_base = os.path.splitext(object_base_image)[0] # Construct the initial object image path (0 degrees rotation) object_path = f'/captcha_data/{puzzle_type}/{object_base}_0.png' additional_data = { "reference_image": ref_path, "object_image": object_path, "object_base": object_base, "current_angle": 0 } # For Slide_Puzzle, include the component image path and target position data elif puzzle_type == "Slide_Puzzle": # Get component image name component_image = ground_truth[selected_puzzle].get("component_image") if not component_image: # If missing required fields, try another puzzle or fall back return jsonify({'error': f'Invalid slide puzzle data: {selected_puzzle}'}), 500 # Format path for the component image component_path = f'/captcha_data/{puzzle_type}/{component_image}' additional_data = { "component_image": component_path, "background_image": f'/captcha_data/{puzzle_type}/{selected_puzzle}' } # For Unusual_Detection, include the grid size elif puzzle_type == "Unusual_Detection": # Get grid size from ground truth grid_size = ground_truth[selected_puzzle].get("grid_size", [2, 3]) # Default to 2x3 grid if not specified additional_data = { "grid_size": grid_size } # For Image_Recognition, include the grid images elif puzzle_type == "Image_Recognition": # Get images array from ground truth images = ground_truth[selected_puzzle].get("images", []) grid_size = [3, 3] # Default grid size for image recognition (3x3) # Get the subfolder name from the puzzle_id or use a specific subfolder field subfolder = ground_truth[selected_puzzle].get("subfolder", selected_puzzle) # Include image paths in response - dynamically use the subfolder image_paths = [f'/captcha_data/{puzzle_type}/{subfolder}/{img}' for img in images] additional_data = { "images": image_paths, "grid_size": grid_size, "question": ground_truth[selected_puzzle].get("question", "Select matching images") } # For Bingo, include the grid size elif puzzle_type == "Bingo": # Get grid size from ground truth grid_size = ground_truth[selected_puzzle].get("grid_size", [3, 3]) # Default to 3x3 grid if not specified additional_data = { "grid_size": grid_size, "solution_line": ground_truth[selected_puzzle].get("solution_line", {}), "answer": ground_truth[selected_puzzle].get("answer", []) } # For Image_Matching, include the reference image and options elif puzzle_type == "Image_Matching": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") option_images = ground_truth[selected_puzzle].get("option_images", []) correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0) if not reference_image or not option_images: return jsonify({'error': f'Invalid image matching data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option_index } # For Patch_Select, include the grid size and target object elif puzzle_type == "Patch_Select": # Get grid size from ground truth, default to 6x6 grid grid_size = ground_truth[selected_puzzle].get("grid_size", [5, 5]) target_object = ground_truth[selected_puzzle].get("target_object", "moon") correct_patches = ground_truth[selected_puzzle].get("correct_patches", []) additional_data = { "grid_size": grid_size, "target_object": target_object, "correct_patches": correct_patches } # For Dart_Count, include the reference image and options elif puzzle_type == "Dart_Count": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") option_images = ground_truth[selected_puzzle].get("option_images", []) correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0) reference_number = ground_truth[selected_puzzle].get("reference_number", 0) if not reference_image or not option_images: return jsonify({'error': f'Invalid dart count data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option_index, "reference_number": reference_number } # For Object_Match, include the reference image and options elif puzzle_type == "Object_Match": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") option_images = ground_truth[selected_puzzle].get("option_images", []) correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0) if not reference_image or not option_images: return jsonify({'error': f'Invalid object match data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option_index } # For Select_Animal, include the grid size and target object elif puzzle_type == "Select_Animal": # Get grid size from ground truth, default to 2x3 grid grid_size = ground_truth[selected_puzzle].get("grid_size", [2, 3]) target_object = ground_truth[selected_puzzle].get("target_object", "fox") correct_patches = ground_truth[selected_puzzle].get("correct_patches", []) additional_data = { "grid_size": grid_size, "target_object": target_object, "correct_patches": correct_patches } # For Coordinates, include the reference image and options elif puzzle_type == "Coordinates": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") option_images = ground_truth[selected_puzzle].get("option_images", []) correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0) if not reference_image or not option_images: return jsonify({'error': f'Invalid coordinates data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option_index } # For Path_Finder, include the reference image and options elif puzzle_type == "Path_Finder": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") options = ground_truth[selected_puzzle].get("options", []) correct_option = ground_truth[selected_puzzle].get("correct_option", 0) if not reference_image or not options: return jsonify({'error': f'Invalid path finder data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in options] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option } # For Connect_icon, include the reference image and options elif puzzle_type == "Connect_icon": # Get the reference image and option images reference_image = ground_truth[selected_puzzle].get("reference_image") options = ground_truth[selected_puzzle].get("options", []) correct_option = ground_truth[selected_puzzle].get("correct_option", 0) if not reference_image or not options: return jsonify({'error': f'Invalid connect icons data: {selected_puzzle}'}), 500 # Format paths for these images ref_path = f'/captcha_data/{puzzle_type}/{reference_image}' option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in options] additional_data = { "reference_image": ref_path, "option_images": option_paths, "current_option_index": 0, "correct_option_index": correct_option } # For Click_Order, include the order image path elif puzzle_type == "Click_Order": # Get the order image from ground truth order_image = ground_truth[selected_puzzle].get("order_image") if not order_image: return jsonify({'error': f'Invalid click order data: {selected_puzzle}'}), 500 # Format path for the order image order_path = f'/captcha_data/{puzzle_type}/{order_image}' additional_data = { "order_image": order_path, "tolerance": ground_truth[selected_puzzle].get("tolerance", 20) } # For Hold_Button, include the hold time elif puzzle_type == "Hold_Button": # Get the required hold time from ground truth hold_time = ground_truth[selected_puzzle].get("hold_time", 3) # Default to 3 seconds if not specified additional_data = { "hold_time": hold_time } # For Misleading_Click, include the area to avoid elif puzzle_type == "Misleading_Click": # Get the area to avoid from ground truth avoid_area = ground_truth[selected_puzzle].get("avoid_area", {"x": 0, "y": 0, "width": 0, "height": 0}) additional_data = { "avoid_area": avoid_area } else: prompt = ground_truth[selected_puzzle].get("prompt", "Solve the CAPTCHA puzzle") response_data = { 'puzzle_type': puzzle_type, 'image_path': f'/captcha_data/{puzzle_type}/{selected_puzzle}' if puzzle_type != "Rotation_Match" else None, 'puzzle_id': selected_puzzle, 'prompt': prompt, 'input_type': input_type, 'debug_info': f"Type: {puzzle_type}, Input: {input_type}, Puzzle: {selected_puzzle}" } # Add any additional data for specific puzzle types if additional_data: response_data.update(additional_data) return jsonify(response_data) @app.route('/api/get_ground_truth', methods=['POST']) def get_ground_truth(): """Return ground truth data for debugging purposes""" data = request.json puzzle_type = data.get('puzzle_type') puzzle_id = data.get('puzzle_id') if not puzzle_type or not puzzle_id: return jsonify({'error': 'Missing puzzle_type or puzzle_id'}), 400 ground_truth = load_ground_truth(puzzle_type) if puzzle_id not in ground_truth: return jsonify({'error': 'Invalid puzzle ID'}), 400 # Return the ground truth for the specified puzzle puzzle_data = ground_truth[puzzle_id] # For Place_Dot puzzles, include the target_position and tolerance in the answer if puzzle_type == 'Place_Dot': return jsonify({ 'answer': { 'target_position': puzzle_data.get('target_position'), 'tolerance': puzzle_data.get('tolerance', 15) }, 'question': puzzle_data.get('question'), 'description': puzzle_data.get('description') }) # For Misleading_Click puzzles, ensure avoid_area is included in the answer elif puzzle_type == 'Misleading_Click': return jsonify({ 'answer': { 'avoid_area': puzzle_data.get('avoid_area', {"x": 0, "y": 0, "width": 0, "height": 0}) }, 'prompt': puzzle_data.get('prompt'), 'description': puzzle_data.get('description') }) return jsonify({ 'answer': puzzle_data.get('answer'), 'question': puzzle_data.get('question'), 'description': puzzle_data.get('description') }) @app.route('/api/check_answer', methods=['POST']) def check_answer(): data = request.json puzzle_type = data.get('puzzle_type', 'Dice_Count') puzzle_id = data.get('puzzle_id') user_answer = data.get('answer') elapsed_time = float(data.get('elapsed_time', 0)) # Validate input if not puzzle_id or user_answer is None: return jsonify({'error': 'Missing puzzle_id or answer'}), 400 ground_truth = load_ground_truth(puzzle_type) if puzzle_id not in ground_truth: return jsonify({'error': 'Invalid puzzle ID'}), 400 # Get correct answer based on puzzle type is_correct = False if puzzle_type == 'Dice_Count': # For dice count, ensure we're comparing numbers try: correct_answer = ground_truth[puzzle_id].get('sum') is_correct = int(user_answer) == int(correct_answer) except ValueError: return jsonify({'error': 'Invalid answer format'}), 400 elif puzzle_type == 'Geometry_Click': # For geometry click, check if click is within the correct area try: # Get the area boundaries from ground truth correct_answer = ground_truth[puzzle_id].get('answer') # Extract coordinates user_x, user_y = user_answer # Check if the new format is used (with area) if isinstance(correct_answer, dict) and 'area' in correct_answer: # Get area coordinates (top-left and bottom-right corners) top_left, bottom_right = correct_answer['area'] min_x, min_y = top_left max_x, max_y = bottom_right # Check if click is within the defined area is_correct = (min_x <= user_x <= max_x) and (min_y <= user_y <= max_y) # Return the shape type as part of the correct answer shape_type = correct_answer.get('type', 'shape') correct_answer_info = { 'type': shape_type, 'area': correct_answer['area'] } else: # Fall back to the old format with distance calculation correct_x, correct_y = correct_answer # Calculate distance and check if within tolerance (25 pixels) tolerance = 25 distance = ((user_x - correct_x) ** 2 + (user_y - correct_y) ** 2) ** 0.5 is_correct = distance <= tolerance correct_answer_info = correct_answer except (ValueError, TypeError, KeyError): return jsonify({'error': 'Invalid answer format for Geometry_Click'}), 400 elif puzzle_type == 'Rotation_Match': # For rotation match, check if the angle matches the correct answer try: # Get the correct angle from ground truth correct_angle = ground_truth[puzzle_id].get('correct_angle') # User answer should be the current rotation angle user_angle = int(user_answer) # Check if angles match (using modulo to handle full rotations) is_correct = user_angle % 360 == correct_angle % 360 correct_answer_info = correct_angle except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Rotation_Match'}), 400 elif puzzle_type == 'Slide_Puzzle': # For slide puzzle, check if the component is positioned correctly try: # Get the target position from ground truth target_position = ground_truth[puzzle_id].get('target_position') tolerance = ground_truth[puzzle_id].get('tolerance', 10) # User answer should be the final position coordinates [x, y] user_x, user_y = user_answer target_x, target_y = target_position # Calculate distance from target position distance = ((user_x - target_x) ** 2 + (user_y - target_y) ** 2) ** 0.5 # Check if within tolerance is_correct = distance <= tolerance correct_answer_info = target_position except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Slide_Puzzle'}), 400 elif puzzle_type == 'Unusual_Detection': # For unusual detection, check if the selected grid cells match the unusual ones try: # Get the expected unusual cells from ground truth correct_cells = ground_truth[puzzle_id].get('answer', []) # User answer should be a list of selected grid cell indices user_cells = user_answer # Check if the selected cells match exactly is_correct = set(user_cells) == set(correct_cells) correct_answer_info = correct_cells except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Unusual_Detection'}), 400 elif puzzle_type == 'Image_Recognition': # For image recognition, check if the selected images match the expected ones try: # Get the expected correct image indices from ground truth correct_selections = ground_truth[puzzle_id].get('correct_selections', []) # User answer should be a list of selected image indices user_selections = user_answer # Check if the selected images match exactly is_correct = set(user_selections) == set(correct_selections) correct_answer_info = correct_selections except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Image_Recognition'}), 400 elif puzzle_type == 'Bingo': # For Bingo, check if the swapped positions would create a line of matching images try: # Get the expected correct swap options from ground truth correct_swaps = ground_truth[puzzle_id].get('answer', []) # User answer should be a list of two indices to swap user_swaps = user_answer # Check if the swaps match any of the possible correct swaps # For this puzzle, there can be multiple correct solutions is_correct = False # Go through each possible solution for correct_swap in correct_swaps: # Check if user's swap matches this solution (order doesn't matter) if (set(user_swaps) == set(correct_swap) or (set(user_swaps) == set(correct_swap[::-1]) if len(correct_swap) == 2 else False)): is_correct = True break correct_answer_info = correct_swaps except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Bingo'}), 400 elif puzzle_type == 'Image_Matching': # For Image Matching, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option_index') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Image_Matching'}), 400 elif puzzle_type == 'Patch_Select': # For Patch_Select, check if the selected patches match the correct ones try: # Get the correct patches from ground truth correct_patches = ground_truth[puzzle_id].get('correct_patches', []) # User answer should be a list of selected patch indices user_patches = user_answer # Check if the selected patches match exactly is_correct = set(user_patches) == set(correct_patches) correct_answer_info = correct_patches except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Patch_Select'}), 400 elif puzzle_type == 'Dart_Count': # For Dart_Count, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option_index') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Dart_Count'}), 400 elif puzzle_type == 'Place_Dot': # For Place_Dot, check if the dot is placed at the end of the car's path try: # Get the target position from ground truth target_position = ground_truth[puzzle_id].get('target_position') tolerance = ground_truth[puzzle_id].get('tolerance', 15) # Default tolerance of 15 pixels # Extract coordinates from user's answer (click position) user_x, user_y = user_answer target_x, target_y = target_position # Calculate distance from target position distance = ((user_x - target_x) ** 2 + (user_y - target_y) ** 2) ** 0.5 # Check if within tolerance is_correct = distance <= tolerance correct_answer_info = target_position except (ValueError, TypeError, KeyError): return jsonify({'error': 'Invalid answer format for Place_Dot'}), 400 elif puzzle_type == 'Object_Match': # For Object_Match, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option_index') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Object_Match'}), 400 elif puzzle_type == 'Select_Animal': # For Select_Animal, check if the selected patches match the correct ones try: # Get the correct patches from ground truth correct_patches = ground_truth[puzzle_id].get('correct_patches', []) # User answer should be a list of selected patch indices user_patches = user_answer # Check if the selected patches match exactly is_correct = set(user_patches) == set(correct_patches) correct_answer_info = correct_patches except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Select_Animal'}), 400 elif puzzle_type == 'Coordinates': # For Coordinates, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option_index') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Coordinates'}), 400 elif puzzle_type == 'Path_Finder': # For Path_Finder, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Path_Finder'}), 400 elif puzzle_type == 'Connect_icon': # For Connect_icon, check if the selected option index matches the correct one try: # Get the correct option index from ground truth correct_index = ground_truth[puzzle_id].get('correct_option') # User answer should be the selected option index user_index = int(user_answer) # Check if indices match is_correct = user_index == correct_index correct_answer_info = correct_index except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Connect_icon'}), 400 elif puzzle_type == 'Click_Order': # For Click_Order, check if the clicked positions match the expected order try: # Get the correct coordinates and tolerance from ground truth correct_positions = ground_truth[puzzle_id].get('answer', []) tolerance = ground_truth[puzzle_id].get('tolerance', 20) # Default tolerance of 20 pixels # User answer should be a list of clicked positions in order user_positions = user_answer # Check if the number of clicks matches if len(user_positions) != len(correct_positions): is_correct = False else: # Check each position with tolerance is_correct = True for i, (user_pos, correct_pos) in enumerate(zip(user_positions, correct_positions)): user_x, user_y = user_pos correct_x, correct_y = correct_pos # Calculate distance distance = ((user_x - correct_x) ** 2 + (user_y - correct_y) ** 2) ** 0.5 # If any position is outside tolerance, the answer is incorrect if distance > tolerance: is_correct = False break correct_answer_info = correct_positions except (ValueError, TypeError, KeyError): return jsonify({'error': 'Invalid answer format for Click_Order'}), 400 elif puzzle_type == 'Hold_Button': # For Hold_Button, check if the hold time is within the allowed range try: # Get the required hold time from ground truth hold_time = ground_truth[puzzle_id].get("hold_time", 3) # Default to 3 seconds if not specified # User answer should be a number representing the hold time in seconds user_hold_time = float(user_answer) if elapsed_time > 8 and user_hold_time < hold_time: is_correct = False correct_answer_info = f"Timeout ({elapsed_time:.2f}s)" else: is_correct = hold_time >= user_hold_time >= 0 correct_answer_info = hold_time except (ValueError, TypeError): return jsonify({'error': 'Invalid answer format for Hold_Button'}), 400 elif puzzle_type == 'Misleading_Click': # For Misleading_Click, check if the click is NOT within the area to avoid try: # Get the area to avoid from ground truth avoid_area = ground_truth[puzzle_id].get("avoid_area", {"x": 0, "y": 0, "width": 0, "height": 0}) # Extract coordinates from user's answer (click position) user_x, user_y = user_answer # Check if click is outside the area to avoid area_x = avoid_area["x"] area_y = avoid_area["y"] area_width = avoid_area["width"] area_height = avoid_area["height"] # Click should be outside the avoid area to be correct is_inside_avoid_area = ( area_x <= user_x <= area_x + area_width and area_y <= user_y <= area_y + area_height ) # User is correct if they clicked outside the avoid area is_correct = not is_inside_avoid_area correct_answer_info = "Click outside the red bear area" except (ValueError, TypeError, KeyError): return jsonify({'error': 'Invalid answer format for Misleading_Click'}), 400 elif puzzle_type == 'Pick_Area': # For Pick_Area, check if click is within the correct area try: # Get the area boundaries from ground truth correct_answer = ground_truth[puzzle_id].get('answer') # Extract coordinates user_x, user_y = user_answer # Check if the correct area is defined if isinstance(correct_answer, dict) and 'area' in correct_answer: # Get area coordinates (top-left and bottom-right corners) top_left, bottom_right = correct_answer['area'] min_x, min_y = top_left max_x, max_y = bottom_right # Check if click is within the defined area is_correct = (min_x <= user_x <= max_x) and (min_y <= user_y <= max_y) # Return the area type as part of the correct answer area_type = correct_answer.get('type', 'largest region') correct_answer_info = { 'type': area_type, 'area': correct_answer['area'] } else: # Fall back if area is not properly defined is_correct = False correct_answer_info = correct_answer except (ValueError, TypeError, KeyError): return jsonify({'error': 'Invalid answer format for Pick_Area'}), 400 else: # For other types, compare as strings (case insensitive) correct_answer = ground_truth[puzzle_id].get('answer') is_correct = str(user_answer).lower() == str(correct_answer).lower() correct_answer_info = correct_answer # Get the appropriate answer field based on puzzle type if puzzle_type == 'Dice_Count': answer_key = 'sum' elif puzzle_type == 'Patch_Select': answer_key = 'correct_patches' elif puzzle_type == 'Select_Animal': answer_key = 'correct_patches' elif puzzle_type == 'Coordinates': answer_key = 'correct_option_index' elif puzzle_type == 'Path_Finder': answer_key = 'correct_option' elif puzzle_type == 'Connect_icon': answer_key = 'correct_option' elif puzzle_type == 'Click_Order': answer_key = 'answer' elif puzzle_type == 'Hold_Button': answer_key = 'hold_time' elif puzzle_type == 'Misleading_Click': answer_key = 'answer' elif puzzle_type == 'Pick_Area': answer_key = 'answer' else: answer_key = 'answer' return jsonify({ 'correct': is_correct, 'user_answer': user_answer, 'correct_answer': ground_truth[puzzle_id].get(answer_key) }) @app.route('/api/benchmark_results', methods=['POST']) def record_benchmark(): data = request.json # Add timestamp if not provided if 'timestamp' not in data: from datetime import datetime data['timestamp'] = datetime.now().isoformat() # In a real system, you would save this data to a database # For this example, we'll just print it to the console print(f"Benchmark results: {data}") # You could store this in a log file as well with open('benchmark_results.json', 'a') as f: f.write(json.dumps(data) + '\n') return jsonify({'status': 'success'}) @app.route('/api/types', methods=['GET']) def get_types(): """Get available CAPTCHA types""" return jsonify({ 'types': get_captcha_types() }) if __name__ == '__main__': # For local development if os.environ.get('DEVELOPMENT'): app.run(debug=True) else: # For production on Hugging Face Spaces app.run(host='0.0.0.0', port=7860)