Spaces:
Sleeping
Sleeping
File size: 44,822 Bytes
105e821 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 |
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/<captcha_type>/<filename>')
def serve_captcha(captcha_type, filename):
return send_from_directory(os.path.join('captcha_data', captcha_type), filename)
@app.route('/captcha_data/<captcha_type>/<subdir>/<filename>')
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) |