Spaces:
Running
Running
| 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))] | |
| def index(): | |
| return render_template('index.html') | |
| def serve_captcha(captcha_type, filename): | |
| return send_from_directory(os.path.join('captcha_data', captcha_type), filename) | |
| def serve_captcha_subdir(captcha_type, subdir, filename): | |
| return send_from_directory(os.path.join('captcha_data', captcha_type, subdir), filename) | |
| 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) | |
| 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') | |
| }) | |
| 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) | |
| }) | |
| 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'}) | |
| 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) |