OpenCaptchaWorld commited on
Commit
105e821
·
verified ·
1 Parent(s): a5800aa

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +11 -0
  2. app.py +1015 -0
  3. static/.DS_Store +0 -0
  4. static/css/style.css +1074 -0
  5. static/js/script.js +0 -0
  6. templates/index.html +96 -0
Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . /app/
6
+
7
+ RUN pip install --no-cache-dir -r requirements.txt
8
+
9
+ EXPOSE 7860
10
+
11
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,1015 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import random
4
+ from flask import Flask, render_template, request, jsonify, send_from_directory
5
+
6
+ app = Flask(__name__, static_folder='static', template_folder='templates')
7
+
8
+ # Dictionary to track which puzzles have been shown for each CAPTCHA type
9
+ seen_puzzles = {}
10
+ # List to track recently used CAPTCHA types to avoid repetition
11
+ recent_types = []
12
+ # How many types to remember before allowing repetition
13
+ MAX_RECENT_TYPES = 5
14
+
15
+ PUZZLE_TYPE_SEQUENCE = [
16
+ 'Dice_Count',
17
+ 'Geometry_Click',
18
+ 'Rotation_Match',
19
+ 'Slide_Puzzle',
20
+ 'Unusual_Detection',
21
+ 'Image_Recognition',
22
+ 'Bingo',
23
+ 'Image_Matching',
24
+ 'Patch_Select',
25
+ 'Dart_Count',
26
+ 'Object_Match',
27
+ 'Select_Animal',
28
+ 'Coordinates',
29
+ 'Path_Finder',
30
+ 'Place_Dot',
31
+ 'Connect_icon',
32
+ 'Click_Order',
33
+ 'Hold_Button',
34
+ 'Misleading_Click',
35
+ 'Pick_Area'
36
+ ]
37
+ sequential_index = 0
38
+
39
+ # Load ground truth data for a specific type
40
+ def load_ground_truth(captcha_type):
41
+ path = os.path.join('captcha_data', captcha_type, 'ground_truth.json')
42
+ try:
43
+ with open(path, 'r') as f:
44
+ return json.load(f)
45
+ except (FileNotFoundError, json.JSONDecodeError):
46
+ return {}
47
+
48
+ # Get available CAPTCHA types
49
+ def get_captcha_types():
50
+ base_dir = 'captcha_data'
51
+ if not os.path.exists(base_dir):
52
+ return []
53
+ return [d for d in os.listdir(base_dir)
54
+ if os.path.isdir(os.path.join(base_dir, d))]
55
+
56
+ @app.route('/')
57
+ def index():
58
+ return render_template('index.html')
59
+
60
+ @app.route('/captcha_data/<captcha_type>/<filename>')
61
+ def serve_captcha(captcha_type, filename):
62
+ return send_from_directory(os.path.join('captcha_data', captcha_type), filename)
63
+
64
+ @app.route('/captcha_data/<captcha_type>/<subdir>/<filename>')
65
+ def serve_captcha_subdir(captcha_type, subdir, filename):
66
+ return send_from_directory(os.path.join('captcha_data', captcha_type, subdir), filename)
67
+
68
+ @app.route('/api/get_puzzle', methods=['GET'])
69
+ def get_puzzle():
70
+ global recent_types
71
+
72
+ # Check if we should return a random puzzle from any type
73
+ is_random = request.args.get('random', 'false').lower() == 'true'
74
+
75
+ # Get all available CAPTCHA types
76
+ captcha_types = get_captcha_types()
77
+ if not captcha_types:
78
+ return jsonify({'error': 'No CAPTCHA types found'}), 404
79
+
80
+ # Check if we're in debug mode for a specific type
81
+ debug_type = request.args.get('debug_type')
82
+
83
+ mode = request.args.get('mode', '').lower()
84
+
85
+ if debug_type and debug_type in captcha_types:
86
+ puzzle_type = debug_type
87
+ elif not is_random and mode == 'sequential':
88
+ global sequential_index
89
+ puzzle_type = PUZZLE_TYPE_SEQUENCE[sequential_index % len(PUZZLE_TYPE_SEQUENCE)]
90
+ sequential_index += 1
91
+ elif is_random:
92
+ # Select a random CAPTCHA type, avoiding recently used types if possible
93
+ available_types = [t for t in captcha_types if t not in recent_types]
94
+
95
+ # If all types have been used recently, reset the tracking
96
+ if not available_types:
97
+ recent_types = []
98
+ available_types = captcha_types
99
+
100
+ puzzle_type = random.choice(available_types)
101
+
102
+ # Add to recent types and maintain maximum length
103
+ recent_types.append(puzzle_type)
104
+ if len(recent_types) > MAX_RECENT_TYPES:
105
+ recent_types.pop(0)
106
+ else:
107
+ # Get puzzle type from query parameter
108
+ puzzle_type = request.args.get('type', 'Dice_Count')
109
+ # Check if puzzle type exists
110
+ if puzzle_type not in captcha_types:
111
+ return jsonify({'error': f'Invalid puzzle type: {puzzle_type}'}), 400
112
+
113
+ # Load ground truth for the selected type
114
+ ground_truth = load_ground_truth(puzzle_type)
115
+ if not ground_truth:
116
+ return jsonify({'error': f'No puzzles found for type: {puzzle_type}'}), 404
117
+
118
+ puzzle_files = list(ground_truth.keys())
119
+
120
+ # Select a random puzzle, avoiding repetition if possible
121
+ if puzzle_type not in seen_puzzles:
122
+ seen_puzzles[puzzle_type] = set()
123
+
124
+ # Get unseen puzzles
125
+ unseen_puzzles = [p for p in puzzle_files if p not in seen_puzzles[puzzle_type]]
126
+
127
+ # If all puzzles have been seen, reset the tracking
128
+ if not unseen_puzzles:
129
+ seen_puzzles[puzzle_type] = set()
130
+ unseen_puzzles = puzzle_files
131
+
132
+ # Select a random puzzle from unseen ones
133
+ selected_puzzle = random.choice(unseen_puzzles)
134
+
135
+ # Mark this puzzle as seen
136
+ seen_puzzles[puzzle_type].add(selected_puzzle)
137
+
138
+ # Get the appropriate question prompt based on puzzle type
139
+ if puzzle_type == "Dice_Count":
140
+ prompt = ground_truth[selected_puzzle].get('prompt', "Sum up the numbers on all the dice")
141
+ elif puzzle_type == "Geometry_Click":
142
+ prompt = ground_truth[selected_puzzle].get("question", "Click on the geometric shape")
143
+ elif puzzle_type == "Rotation_Match":
144
+ prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to rotate the object to match the reference direction")
145
+ elif puzzle_type == "Slide_Puzzle":
146
+ prompt = ground_truth[selected_puzzle].get("prompt", "Drag the slider component to the correct position")
147
+ elif puzzle_type == "Unusual_Detection":
148
+ prompt = ground_truth[selected_puzzle].get("prompt", "Select the unusual items in the image")
149
+ elif puzzle_type == "Image_Recognition":
150
+ prompt = ground_truth[selected_puzzle].get("prompt", "Select all images matching the description")
151
+ elif puzzle_type == "Bingo":
152
+ prompt = ground_truth[selected_puzzle].get("prompt", "Please click two images to exchange their position to line up the same images to a line")
153
+ elif puzzle_type == "Image_Matching":
154
+ prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, match the animal in the left and right image.")
155
+ elif puzzle_type == "Patch_Select":
156
+ prompt = ground_truth[selected_puzzle].get("prompt", "Select all squares with the specified objects")
157
+ elif puzzle_type == "Dart_Count":
158
+ 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.")
159
+ elif puzzle_type == "Object_Match":
160
+ prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to change the number of objects until it matches the left image.")
161
+ elif puzzle_type == "Select_Animal":
162
+ prompt = ground_truth[selected_puzzle].get("prompt", "Pick a fox")
163
+ elif puzzle_type == "Coordinates":
164
+ prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, move Jerry to the indicated seat")
165
+ elif puzzle_type == "Path_Finder":
166
+ prompt = ground_truth[selected_puzzle].get("prompt", "Use the arrows to move the duck to the spot indicated by the cross")
167
+ elif puzzle_type == "Place_Dot":
168
+ prompt = ground_truth[selected_puzzle].get("prompt", "Click to place a Dot at the end of the car's path")
169
+ elif puzzle_type == "Connect_icon":
170
+ prompt = ground_truth[selected_puzzle].get("prompt", "Using the arrows, connect the same two icons with the dotted line as shown on the left.")
171
+ elif puzzle_type == "Click_Order":
172
+ prompt = ground_truth[selected_puzzle].get("prompt", "Click the icons in order as shown in the reference image.")
173
+ elif puzzle_type == "Hold_Button":
174
+ prompt = ground_truth[selected_puzzle].get("prompt", "Hold the button until it finishes loading.")
175
+ elif puzzle_type == "Misleading_Click":
176
+ prompt = ground_truth[selected_puzzle].get("prompt", "Click the image to continue.")
177
+ elif puzzle_type == "Pick_Area":
178
+ prompt = ground_truth[selected_puzzle].get("prompt", "Click on the largest area outlined by the dotted line")
179
+ else:
180
+ prompt = ground_truth[selected_puzzle].get("prompt", "Solve the CAPTCHA puzzle")
181
+
182
+ # Add input_type to tell the frontend what kind of input to show
183
+ input_type = "text"
184
+ if puzzle_type == "Dice_Count":
185
+ input_type = "number"
186
+ elif puzzle_type == "Geometry_Click":
187
+ input_type = "click"
188
+ elif puzzle_type == "Rotation_Match":
189
+ input_type = "rotation"
190
+ elif puzzle_type == "Slide_Puzzle":
191
+ input_type = "slide"
192
+ elif puzzle_type == "Unusual_Detection":
193
+ input_type = "multiselect"
194
+ elif puzzle_type == "Image_Recognition":
195
+ input_type = "image_grid"
196
+ elif puzzle_type == "Bingo":
197
+ input_type = "bingo_swap"
198
+ elif puzzle_type == "Image_Matching":
199
+ input_type = "image_matching"
200
+ elif puzzle_type == "Patch_Select":
201
+ input_type = "patch_select"
202
+ elif puzzle_type == "Dart_Count":
203
+ input_type = "dart_count"
204
+ elif puzzle_type == "Object_Match":
205
+ input_type = "object_match"
206
+ elif puzzle_type == "Select_Animal":
207
+ input_type = "select_animal"
208
+ elif puzzle_type == "Coordinates":
209
+ input_type = "image_matching"
210
+ elif puzzle_type == "Path_Finder":
211
+ input_type = "image_matching"
212
+ elif puzzle_type == "Place_Dot":
213
+ input_type = "place_dot"
214
+ elif puzzle_type == "Connect_icon":
215
+ input_type = "connect_icon"
216
+ elif puzzle_type == "Click_Order":
217
+ input_type = "click_order"
218
+ elif puzzle_type == "Hold_Button":
219
+ input_type = "hold_button"
220
+ elif puzzle_type == "Misleading_Click":
221
+ input_type = "click"
222
+ elif puzzle_type == "Pick_Area":
223
+ input_type = "click"
224
+
225
+ # For Rotation_Match, include additional data needed for the interface
226
+ additional_data = {}
227
+ if puzzle_type == "Rotation_Match":
228
+ # Get reference image and object base name
229
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
230
+ object_base_image = ground_truth[selected_puzzle].get("object_base_image")
231
+
232
+ if not reference_image or not object_base_image:
233
+ # If missing required fields, try another puzzle or fall back
234
+ return jsonify({'error': f'Invalid rotation puzzle data: {selected_puzzle}'}), 500
235
+
236
+ # Format paths for these images
237
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
238
+
239
+ # Get object base name without extension to construct rotated image paths
240
+ object_base = os.path.splitext(object_base_image)[0]
241
+
242
+ # Construct the initial object image path (0 degrees rotation)
243
+ object_path = f'/captcha_data/{puzzle_type}/{object_base}_0.png'
244
+
245
+ additional_data = {
246
+ "reference_image": ref_path,
247
+ "object_image": object_path,
248
+ "object_base": object_base,
249
+ "current_angle": 0
250
+ }
251
+ # For Slide_Puzzle, include the component image path and target position data
252
+ elif puzzle_type == "Slide_Puzzle":
253
+ # Get component image name
254
+ component_image = ground_truth[selected_puzzle].get("component_image")
255
+
256
+ if not component_image:
257
+ # If missing required fields, try another puzzle or fall back
258
+ return jsonify({'error': f'Invalid slide puzzle data: {selected_puzzle}'}), 500
259
+
260
+ # Format path for the component image
261
+ component_path = f'/captcha_data/{puzzle_type}/{component_image}'
262
+
263
+ additional_data = {
264
+ "component_image": component_path,
265
+ "background_image": f'/captcha_data/{puzzle_type}/{selected_puzzle}'
266
+ }
267
+ # For Unusual_Detection, include the grid size
268
+ elif puzzle_type == "Unusual_Detection":
269
+ # Get grid size from ground truth
270
+ grid_size = ground_truth[selected_puzzle].get("grid_size", [2, 3]) # Default to 2x3 grid if not specified
271
+
272
+ additional_data = {
273
+ "grid_size": grid_size
274
+ }
275
+ # For Image_Recognition, include the grid images
276
+ elif puzzle_type == "Image_Recognition":
277
+ # Get images array from ground truth
278
+ images = ground_truth[selected_puzzle].get("images", [])
279
+ grid_size = [3, 3] # Default grid size for image recognition (3x3)
280
+
281
+ # Get the subfolder name from the puzzle_id or use a specific subfolder field
282
+ subfolder = ground_truth[selected_puzzle].get("subfolder", selected_puzzle)
283
+
284
+ # Include image paths in response - dynamically use the subfolder
285
+ image_paths = [f'/captcha_data/{puzzle_type}/{subfolder}/{img}' for img in images]
286
+
287
+ additional_data = {
288
+ "images": image_paths,
289
+ "grid_size": grid_size,
290
+ "question": ground_truth[selected_puzzle].get("question", "Select matching images")
291
+ }
292
+ # For Bingo, include the grid size
293
+ elif puzzle_type == "Bingo":
294
+ # Get grid size from ground truth
295
+ grid_size = ground_truth[selected_puzzle].get("grid_size", [3, 3]) # Default to 3x3 grid if not specified
296
+
297
+ additional_data = {
298
+ "grid_size": grid_size,
299
+ "solution_line": ground_truth[selected_puzzle].get("solution_line", {}),
300
+ "answer": ground_truth[selected_puzzle].get("answer", [])
301
+ }
302
+ # For Image_Matching, include the reference image and options
303
+ elif puzzle_type == "Image_Matching":
304
+ # Get the reference image and option images
305
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
306
+ option_images = ground_truth[selected_puzzle].get("option_images", [])
307
+ correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0)
308
+
309
+ if not reference_image or not option_images:
310
+ return jsonify({'error': f'Invalid image matching data: {selected_puzzle}'}), 500
311
+
312
+ # Format paths for these images
313
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
314
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images]
315
+
316
+ additional_data = {
317
+ "reference_image": ref_path,
318
+ "option_images": option_paths,
319
+ "current_option_index": 0,
320
+ "correct_option_index": correct_option_index
321
+ }
322
+ # For Patch_Select, include the grid size and target object
323
+ elif puzzle_type == "Patch_Select":
324
+ # Get grid size from ground truth, default to 6x6 grid
325
+ grid_size = ground_truth[selected_puzzle].get("grid_size", [5, 5])
326
+ target_object = ground_truth[selected_puzzle].get("target_object", "moon")
327
+ correct_patches = ground_truth[selected_puzzle].get("correct_patches", [])
328
+
329
+ additional_data = {
330
+ "grid_size": grid_size,
331
+ "target_object": target_object,
332
+ "correct_patches": correct_patches
333
+ }
334
+ # For Dart_Count, include the reference image and options
335
+ elif puzzle_type == "Dart_Count":
336
+ # Get the reference image and option images
337
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
338
+ option_images = ground_truth[selected_puzzle].get("option_images", [])
339
+ correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0)
340
+ reference_number = ground_truth[selected_puzzle].get("reference_number", 0)
341
+
342
+ if not reference_image or not option_images:
343
+ return jsonify({'error': f'Invalid dart count data: {selected_puzzle}'}), 500
344
+
345
+ # Format paths for these images
346
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
347
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images]
348
+
349
+ additional_data = {
350
+ "reference_image": ref_path,
351
+ "option_images": option_paths,
352
+ "current_option_index": 0,
353
+ "correct_option_index": correct_option_index,
354
+ "reference_number": reference_number
355
+ }
356
+ # For Object_Match, include the reference image and options
357
+ elif puzzle_type == "Object_Match":
358
+ # Get the reference image and option images
359
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
360
+ option_images = ground_truth[selected_puzzle].get("option_images", [])
361
+ correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0)
362
+
363
+ if not reference_image or not option_images:
364
+ return jsonify({'error': f'Invalid object match data: {selected_puzzle}'}), 500
365
+
366
+ # Format paths for these images
367
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
368
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images]
369
+
370
+ additional_data = {
371
+ "reference_image": ref_path,
372
+ "option_images": option_paths,
373
+ "current_option_index": 0,
374
+ "correct_option_index": correct_option_index
375
+ }
376
+ # For Select_Animal, include the grid size and target object
377
+ elif puzzle_type == "Select_Animal":
378
+ # Get grid size from ground truth, default to 2x3 grid
379
+ grid_size = ground_truth[selected_puzzle].get("grid_size", [2, 3])
380
+ target_object = ground_truth[selected_puzzle].get("target_object", "fox")
381
+ correct_patches = ground_truth[selected_puzzle].get("correct_patches", [])
382
+
383
+ additional_data = {
384
+ "grid_size": grid_size,
385
+ "target_object": target_object,
386
+ "correct_patches": correct_patches
387
+ }
388
+ # For Coordinates, include the reference image and options
389
+ elif puzzle_type == "Coordinates":
390
+ # Get the reference image and option images
391
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
392
+ option_images = ground_truth[selected_puzzle].get("option_images", [])
393
+ correct_option_index = ground_truth[selected_puzzle].get("correct_option_index", 0)
394
+
395
+ if not reference_image or not option_images:
396
+ return jsonify({'error': f'Invalid coordinates data: {selected_puzzle}'}), 500
397
+
398
+ # Format paths for these images
399
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
400
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in option_images]
401
+
402
+ additional_data = {
403
+ "reference_image": ref_path,
404
+ "option_images": option_paths,
405
+ "current_option_index": 0,
406
+ "correct_option_index": correct_option_index
407
+ }
408
+ # For Path_Finder, include the reference image and options
409
+ elif puzzle_type == "Path_Finder":
410
+ # Get the reference image and option images
411
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
412
+ options = ground_truth[selected_puzzle].get("options", [])
413
+ correct_option = ground_truth[selected_puzzle].get("correct_option", 0)
414
+
415
+ if not reference_image or not options:
416
+ return jsonify({'error': f'Invalid path finder data: {selected_puzzle}'}), 500
417
+
418
+ # Format paths for these images
419
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
420
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in options]
421
+
422
+ additional_data = {
423
+ "reference_image": ref_path,
424
+ "option_images": option_paths,
425
+ "current_option_index": 0,
426
+ "correct_option_index": correct_option
427
+ }
428
+ # For Connect_icon, include the reference image and options
429
+ elif puzzle_type == "Connect_icon":
430
+ # Get the reference image and option images
431
+ reference_image = ground_truth[selected_puzzle].get("reference_image")
432
+ options = ground_truth[selected_puzzle].get("options", [])
433
+ correct_option = ground_truth[selected_puzzle].get("correct_option", 0)
434
+
435
+ if not reference_image or not options:
436
+ return jsonify({'error': f'Invalid connect icons data: {selected_puzzle}'}), 500
437
+
438
+ # Format paths for these images
439
+ ref_path = f'/captcha_data/{puzzle_type}/{reference_image}'
440
+ option_paths = [f'/captcha_data/{puzzle_type}/{img}' for img in options]
441
+
442
+ additional_data = {
443
+ "reference_image": ref_path,
444
+ "option_images": option_paths,
445
+ "current_option_index": 0,
446
+ "correct_option_index": correct_option
447
+ }
448
+ # For Click_Order, include the order image path
449
+ elif puzzle_type == "Click_Order":
450
+ # Get the order image from ground truth
451
+ order_image = ground_truth[selected_puzzle].get("order_image")
452
+
453
+ if not order_image:
454
+ return jsonify({'error': f'Invalid click order data: {selected_puzzle}'}), 500
455
+
456
+ # Format path for the order image
457
+ order_path = f'/captcha_data/{puzzle_type}/{order_image}'
458
+
459
+ additional_data = {
460
+ "order_image": order_path,
461
+ "tolerance": ground_truth[selected_puzzle].get("tolerance", 20)
462
+ }
463
+ # For Hold_Button, include the hold time
464
+ elif puzzle_type == "Hold_Button":
465
+ # Get the required hold time from ground truth
466
+ hold_time = ground_truth[selected_puzzle].get("hold_time", 3) # Default to 3 seconds if not specified
467
+
468
+ additional_data = {
469
+ "hold_time": hold_time
470
+ }
471
+ # For Misleading_Click, include the area to avoid
472
+ elif puzzle_type == "Misleading_Click":
473
+ # Get the area to avoid from ground truth
474
+ avoid_area = ground_truth[selected_puzzle].get("avoid_area", {"x": 0, "y": 0, "width": 0, "height": 0})
475
+
476
+ additional_data = {
477
+ "avoid_area": avoid_area
478
+ }
479
+ else:
480
+ prompt = ground_truth[selected_puzzle].get("prompt", "Solve the CAPTCHA puzzle")
481
+
482
+ response_data = {
483
+ 'puzzle_type': puzzle_type,
484
+ 'image_path': f'/captcha_data/{puzzle_type}/{selected_puzzle}' if puzzle_type != "Rotation_Match" else None,
485
+ 'puzzle_id': selected_puzzle,
486
+ 'prompt': prompt,
487
+ 'input_type': input_type,
488
+ 'debug_info': f"Type: {puzzle_type}, Input: {input_type}, Puzzle: {selected_puzzle}"
489
+ }
490
+
491
+ # Add any additional data for specific puzzle types
492
+ if additional_data:
493
+ response_data.update(additional_data)
494
+
495
+ return jsonify(response_data)
496
+
497
+ @app.route('/api/get_ground_truth', methods=['POST'])
498
+ def get_ground_truth():
499
+ """Return ground truth data for debugging purposes"""
500
+ data = request.json
501
+ puzzle_type = data.get('puzzle_type')
502
+ puzzle_id = data.get('puzzle_id')
503
+
504
+ if not puzzle_type or not puzzle_id:
505
+ return jsonify({'error': 'Missing puzzle_type or puzzle_id'}), 400
506
+
507
+ ground_truth = load_ground_truth(puzzle_type)
508
+
509
+ if puzzle_id not in ground_truth:
510
+ return jsonify({'error': 'Invalid puzzle ID'}), 400
511
+
512
+ # Return the ground truth for the specified puzzle
513
+ puzzle_data = ground_truth[puzzle_id]
514
+
515
+ # For Place_Dot puzzles, include the target_position and tolerance in the answer
516
+ if puzzle_type == 'Place_Dot':
517
+ return jsonify({
518
+ 'answer': {
519
+ 'target_position': puzzle_data.get('target_position'),
520
+ 'tolerance': puzzle_data.get('tolerance', 15)
521
+ },
522
+ 'question': puzzle_data.get('question'),
523
+ 'description': puzzle_data.get('description')
524
+ })
525
+ # For Misleading_Click puzzles, ensure avoid_area is included in the answer
526
+ elif puzzle_type == 'Misleading_Click':
527
+ return jsonify({
528
+ 'answer': {
529
+ 'avoid_area': puzzle_data.get('avoid_area', {"x": 0, "y": 0, "width": 0, "height": 0})
530
+ },
531
+ 'prompt': puzzle_data.get('prompt'),
532
+ 'description': puzzle_data.get('description')
533
+ })
534
+
535
+ return jsonify({
536
+ 'answer': puzzle_data.get('answer'),
537
+ 'question': puzzle_data.get('question'),
538
+ 'description': puzzle_data.get('description')
539
+ })
540
+
541
+ @app.route('/api/check_answer', methods=['POST'])
542
+ def check_answer():
543
+ data = request.json
544
+ puzzle_type = data.get('puzzle_type', 'Dice_Count')
545
+ puzzle_id = data.get('puzzle_id')
546
+ user_answer = data.get('answer')
547
+ elapsed_time = float(data.get('elapsed_time', 0))
548
+
549
+
550
+ # Validate input
551
+ if not puzzle_id or user_answer is None:
552
+ return jsonify({'error': 'Missing puzzle_id or answer'}), 400
553
+
554
+ ground_truth = load_ground_truth(puzzle_type)
555
+
556
+ if puzzle_id not in ground_truth:
557
+ return jsonify({'error': 'Invalid puzzle ID'}), 400
558
+
559
+ # Get correct answer based on puzzle type
560
+ is_correct = False
561
+
562
+ if puzzle_type == 'Dice_Count':
563
+ # For dice count, ensure we're comparing numbers
564
+ try:
565
+ correct_answer = ground_truth[puzzle_id].get('sum')
566
+ is_correct = int(user_answer) == int(correct_answer)
567
+ except ValueError:
568
+ return jsonify({'error': 'Invalid answer format'}), 400
569
+
570
+ elif puzzle_type == 'Geometry_Click':
571
+ # For geometry click, check if click is within the correct area
572
+ try:
573
+ # Get the area boundaries from ground truth
574
+ correct_answer = ground_truth[puzzle_id].get('answer')
575
+
576
+ # Extract coordinates
577
+ user_x, user_y = user_answer
578
+
579
+ # Check if the new format is used (with area)
580
+ if isinstance(correct_answer, dict) and 'area' in correct_answer:
581
+ # Get area coordinates (top-left and bottom-right corners)
582
+ top_left, bottom_right = correct_answer['area']
583
+ min_x, min_y = top_left
584
+ max_x, max_y = bottom_right
585
+
586
+ # Check if click is within the defined area
587
+ is_correct = (min_x <= user_x <= max_x) and (min_y <= user_y <= max_y)
588
+
589
+ # Return the shape type as part of the correct answer
590
+ shape_type = correct_answer.get('type', 'shape')
591
+ correct_answer_info = {
592
+ 'type': shape_type,
593
+ 'area': correct_answer['area']
594
+ }
595
+ else:
596
+ # Fall back to the old format with distance calculation
597
+ correct_x, correct_y = correct_answer
598
+
599
+ # Calculate distance and check if within tolerance (25 pixels)
600
+ tolerance = 25
601
+ distance = ((user_x - correct_x) ** 2 + (user_y - correct_y) ** 2) ** 0.5
602
+ is_correct = distance <= tolerance
603
+ correct_answer_info = correct_answer
604
+ except (ValueError, TypeError, KeyError):
605
+ return jsonify({'error': 'Invalid answer format for Geometry_Click'}), 400
606
+
607
+ elif puzzle_type == 'Rotation_Match':
608
+ # For rotation match, check if the angle matches the correct answer
609
+ try:
610
+ # Get the correct angle from ground truth
611
+ correct_angle = ground_truth[puzzle_id].get('correct_angle')
612
+
613
+ # User answer should be the current rotation angle
614
+ user_angle = int(user_answer)
615
+
616
+ # Check if angles match (using modulo to handle full rotations)
617
+ is_correct = user_angle % 360 == correct_angle % 360
618
+ correct_answer_info = correct_angle
619
+ except (ValueError, TypeError):
620
+ return jsonify({'error': 'Invalid answer format for Rotation_Match'}), 400
621
+
622
+ elif puzzle_type == 'Slide_Puzzle':
623
+ # For slide puzzle, check if the component is positioned correctly
624
+ try:
625
+ # Get the target position from ground truth
626
+ target_position = ground_truth[puzzle_id].get('target_position')
627
+ tolerance = ground_truth[puzzle_id].get('tolerance', 10)
628
+
629
+ # User answer should be the final position coordinates [x, y]
630
+ user_x, user_y = user_answer
631
+ target_x, target_y = target_position
632
+
633
+ # Calculate distance from target position
634
+ distance = ((user_x - target_x) ** 2 + (user_y - target_y) ** 2) ** 0.5
635
+
636
+ # Check if within tolerance
637
+ is_correct = distance <= tolerance
638
+ correct_answer_info = target_position
639
+ except (ValueError, TypeError):
640
+ return jsonify({'error': 'Invalid answer format for Slide_Puzzle'}), 400
641
+
642
+ elif puzzle_type == 'Unusual_Detection':
643
+ # For unusual detection, check if the selected grid cells match the unusual ones
644
+ try:
645
+ # Get the expected unusual cells from ground truth
646
+ correct_cells = ground_truth[puzzle_id].get('answer', [])
647
+
648
+ # User answer should be a list of selected grid cell indices
649
+ user_cells = user_answer
650
+
651
+ # Check if the selected cells match exactly
652
+ is_correct = set(user_cells) == set(correct_cells)
653
+ correct_answer_info = correct_cells
654
+ except (ValueError, TypeError):
655
+ return jsonify({'error': 'Invalid answer format for Unusual_Detection'}), 400
656
+
657
+ elif puzzle_type == 'Image_Recognition':
658
+ # For image recognition, check if the selected images match the expected ones
659
+ try:
660
+ # Get the expected correct image indices from ground truth
661
+ correct_selections = ground_truth[puzzle_id].get('correct_selections', [])
662
+
663
+ # User answer should be a list of selected image indices
664
+ user_selections = user_answer
665
+
666
+ # Check if the selected images match exactly
667
+ is_correct = set(user_selections) == set(correct_selections)
668
+ correct_answer_info = correct_selections
669
+ except (ValueError, TypeError):
670
+ return jsonify({'error': 'Invalid answer format for Image_Recognition'}), 400
671
+
672
+ elif puzzle_type == 'Bingo':
673
+ # For Bingo, check if the swapped positions would create a line of matching images
674
+ try:
675
+ # Get the expected correct swap options from ground truth
676
+ correct_swaps = ground_truth[puzzle_id].get('answer', [])
677
+
678
+ # User answer should be a list of two indices to swap
679
+ user_swaps = user_answer
680
+
681
+ # Check if the swaps match any of the possible correct swaps
682
+ # For this puzzle, there can be multiple correct solutions
683
+ is_correct = False
684
+
685
+ # Go through each possible solution
686
+ for correct_swap in correct_swaps:
687
+ # Check if user's swap matches this solution (order doesn't matter)
688
+ if (set(user_swaps) == set(correct_swap) or
689
+ (set(user_swaps) == set(correct_swap[::-1]) if len(correct_swap) == 2 else False)):
690
+ is_correct = True
691
+ break
692
+
693
+ correct_answer_info = correct_swaps
694
+ except (ValueError, TypeError):
695
+ return jsonify({'error': 'Invalid answer format for Bingo'}), 400
696
+
697
+ elif puzzle_type == 'Image_Matching':
698
+ # For Image Matching, check if the selected option index matches the correct one
699
+ try:
700
+ # Get the correct option index from ground truth
701
+ correct_index = ground_truth[puzzle_id].get('correct_option_index')
702
+
703
+ # User answer should be the selected option index
704
+ user_index = int(user_answer)
705
+
706
+ # Check if indices match
707
+ is_correct = user_index == correct_index
708
+ correct_answer_info = correct_index
709
+ except (ValueError, TypeError):
710
+ return jsonify({'error': 'Invalid answer format for Image_Matching'}), 400
711
+
712
+ elif puzzle_type == 'Patch_Select':
713
+ # For Patch_Select, check if the selected patches match the correct ones
714
+ try:
715
+ # Get the correct patches from ground truth
716
+ correct_patches = ground_truth[puzzle_id].get('correct_patches', [])
717
+
718
+ # User answer should be a list of selected patch indices
719
+ user_patches = user_answer
720
+
721
+ # Check if the selected patches match exactly
722
+ is_correct = set(user_patches) == set(correct_patches)
723
+ correct_answer_info = correct_patches
724
+ except (ValueError, TypeError):
725
+ return jsonify({'error': 'Invalid answer format for Patch_Select'}), 400
726
+
727
+ elif puzzle_type == 'Dart_Count':
728
+ # For Dart_Count, check if the selected option index matches the correct one
729
+ try:
730
+ # Get the correct option index from ground truth
731
+ correct_index = ground_truth[puzzle_id].get('correct_option_index')
732
+
733
+ # User answer should be the selected option index
734
+ user_index = int(user_answer)
735
+
736
+ # Check if indices match
737
+ is_correct = user_index == correct_index
738
+ correct_answer_info = correct_index
739
+ except (ValueError, TypeError):
740
+ return jsonify({'error': 'Invalid answer format for Dart_Count'}), 400
741
+
742
+ elif puzzle_type == 'Place_Dot':
743
+ # For Place_Dot, check if the dot is placed at the end of the car's path
744
+ try:
745
+ # Get the target position from ground truth
746
+ target_position = ground_truth[puzzle_id].get('target_position')
747
+ tolerance = ground_truth[puzzle_id].get('tolerance', 15) # Default tolerance of 15 pixels
748
+
749
+ # Extract coordinates from user's answer (click position)
750
+ user_x, user_y = user_answer
751
+ target_x, target_y = target_position
752
+
753
+ # Calculate distance from target position
754
+ distance = ((user_x - target_x) ** 2 + (user_y - target_y) ** 2) ** 0.5
755
+
756
+ # Check if within tolerance
757
+ is_correct = distance <= tolerance
758
+ correct_answer_info = target_position
759
+ except (ValueError, TypeError, KeyError):
760
+ return jsonify({'error': 'Invalid answer format for Place_Dot'}), 400
761
+
762
+ elif puzzle_type == 'Object_Match':
763
+ # For Object_Match, check if the selected option index matches the correct one
764
+ try:
765
+ # Get the correct option index from ground truth
766
+ correct_index = ground_truth[puzzle_id].get('correct_option_index')
767
+
768
+ # User answer should be the selected option index
769
+ user_index = int(user_answer)
770
+
771
+ # Check if indices match
772
+ is_correct = user_index == correct_index
773
+ correct_answer_info = correct_index
774
+ except (ValueError, TypeError):
775
+ return jsonify({'error': 'Invalid answer format for Object_Match'}), 400
776
+
777
+ elif puzzle_type == 'Select_Animal':
778
+ # For Select_Animal, check if the selected patches match the correct ones
779
+ try:
780
+ # Get the correct patches from ground truth
781
+ correct_patches = ground_truth[puzzle_id].get('correct_patches', [])
782
+
783
+ # User answer should be a list of selected patch indices
784
+ user_patches = user_answer
785
+
786
+ # Check if the selected patches match exactly
787
+ is_correct = set(user_patches) == set(correct_patches)
788
+ correct_answer_info = correct_patches
789
+ except (ValueError, TypeError):
790
+ return jsonify({'error': 'Invalid answer format for Select_Animal'}), 400
791
+
792
+ elif puzzle_type == 'Coordinates':
793
+ # For Coordinates, check if the selected option index matches the correct one
794
+ try:
795
+ # Get the correct option index from ground truth
796
+ correct_index = ground_truth[puzzle_id].get('correct_option_index')
797
+
798
+ # User answer should be the selected option index
799
+ user_index = int(user_answer)
800
+
801
+ # Check if indices match
802
+ is_correct = user_index == correct_index
803
+ correct_answer_info = correct_index
804
+ except (ValueError, TypeError):
805
+ return jsonify({'error': 'Invalid answer format for Coordinates'}), 400
806
+
807
+ elif puzzle_type == 'Path_Finder':
808
+ # For Path_Finder, check if the selected option index matches the correct one
809
+ try:
810
+ # Get the correct option index from ground truth
811
+ correct_index = ground_truth[puzzle_id].get('correct_option')
812
+
813
+ # User answer should be the selected option index
814
+ user_index = int(user_answer)
815
+
816
+ # Check if indices match
817
+ is_correct = user_index == correct_index
818
+ correct_answer_info = correct_index
819
+ except (ValueError, TypeError):
820
+ return jsonify({'error': 'Invalid answer format for Path_Finder'}), 400
821
+
822
+ elif puzzle_type == 'Connect_icon':
823
+ # For Connect_icon, check if the selected option index matches the correct one
824
+ try:
825
+ # Get the correct option index from ground truth
826
+ correct_index = ground_truth[puzzle_id].get('correct_option')
827
+
828
+ # User answer should be the selected option index
829
+ user_index = int(user_answer)
830
+
831
+ # Check if indices match
832
+ is_correct = user_index == correct_index
833
+ correct_answer_info = correct_index
834
+ except (ValueError, TypeError):
835
+ return jsonify({'error': 'Invalid answer format for Connect_icon'}), 400
836
+
837
+ elif puzzle_type == 'Click_Order':
838
+ # For Click_Order, check if the clicked positions match the expected order
839
+ try:
840
+ # Get the correct coordinates and tolerance from ground truth
841
+ correct_positions = ground_truth[puzzle_id].get('answer', [])
842
+ tolerance = ground_truth[puzzle_id].get('tolerance', 20) # Default tolerance of 20 pixels
843
+
844
+ # User answer should be a list of clicked positions in order
845
+ user_positions = user_answer
846
+
847
+ # Check if the number of clicks matches
848
+ if len(user_positions) != len(correct_positions):
849
+ is_correct = False
850
+ else:
851
+ # Check each position with tolerance
852
+ is_correct = True
853
+ for i, (user_pos, correct_pos) in enumerate(zip(user_positions, correct_positions)):
854
+ user_x, user_y = user_pos
855
+ correct_x, correct_y = correct_pos
856
+
857
+ # Calculate distance
858
+ distance = ((user_x - correct_x) ** 2 + (user_y - correct_y) ** 2) ** 0.5
859
+
860
+ # If any position is outside tolerance, the answer is incorrect
861
+ if distance > tolerance:
862
+ is_correct = False
863
+ break
864
+
865
+ correct_answer_info = correct_positions
866
+ except (ValueError, TypeError, KeyError):
867
+ return jsonify({'error': 'Invalid answer format for Click_Order'}), 400
868
+
869
+ elif puzzle_type == 'Hold_Button':
870
+ # For Hold_Button, check if the hold time is within the allowed range
871
+ try:
872
+ # Get the required hold time from ground truth
873
+ hold_time = ground_truth[puzzle_id].get("hold_time", 3) # Default to 3 seconds if not specified
874
+
875
+ # User answer should be a number representing the hold time in seconds
876
+ user_hold_time = float(user_answer)
877
+
878
+ if elapsed_time > 8 and user_hold_time < hold_time:
879
+ is_correct = False
880
+ correct_answer_info = f"Timeout ({elapsed_time:.2f}s)"
881
+ else:
882
+ is_correct = hold_time >= user_hold_time >= 0
883
+ correct_answer_info = hold_time
884
+
885
+ except (ValueError, TypeError):
886
+ return jsonify({'error': 'Invalid answer format for Hold_Button'}), 400
887
+
888
+ elif puzzle_type == 'Misleading_Click':
889
+ # For Misleading_Click, check if the click is NOT within the area to avoid
890
+ try:
891
+ # Get the area to avoid from ground truth
892
+ avoid_area = ground_truth[puzzle_id].get("avoid_area", {"x": 0, "y": 0, "width": 0, "height": 0})
893
+
894
+ # Extract coordinates from user's answer (click position)
895
+ user_x, user_y = user_answer
896
+
897
+ # Check if click is outside the area to avoid
898
+ area_x = avoid_area["x"]
899
+ area_y = avoid_area["y"]
900
+ area_width = avoid_area["width"]
901
+ area_height = avoid_area["height"]
902
+
903
+ # Click should be outside the avoid area to be correct
904
+ is_inside_avoid_area = (
905
+ area_x <= user_x <= area_x + area_width and
906
+ area_y <= user_y <= area_y + area_height
907
+ )
908
+
909
+ # User is correct if they clicked outside the avoid area
910
+ is_correct = not is_inside_avoid_area
911
+ correct_answer_info = "Click outside the red bear area"
912
+ except (ValueError, TypeError, KeyError):
913
+ return jsonify({'error': 'Invalid answer format for Misleading_Click'}), 400
914
+
915
+ elif puzzle_type == 'Pick_Area':
916
+ # For Pick_Area, check if click is within the correct area
917
+ try:
918
+ # Get the area boundaries from ground truth
919
+ correct_answer = ground_truth[puzzle_id].get('answer')
920
+
921
+ # Extract coordinates
922
+ user_x, user_y = user_answer
923
+
924
+ # Check if the correct area is defined
925
+ if isinstance(correct_answer, dict) and 'area' in correct_answer:
926
+ # Get area coordinates (top-left and bottom-right corners)
927
+ top_left, bottom_right = correct_answer['area']
928
+ min_x, min_y = top_left
929
+ max_x, max_y = bottom_right
930
+
931
+ # Check if click is within the defined area
932
+ is_correct = (min_x <= user_x <= max_x) and (min_y <= user_y <= max_y)
933
+
934
+ # Return the area type as part of the correct answer
935
+ area_type = correct_answer.get('type', 'largest region')
936
+ correct_answer_info = {
937
+ 'type': area_type,
938
+ 'area': correct_answer['area']
939
+ }
940
+ else:
941
+ # Fall back if area is not properly defined
942
+ is_correct = False
943
+ correct_answer_info = correct_answer
944
+ except (ValueError, TypeError, KeyError):
945
+ return jsonify({'error': 'Invalid answer format for Pick_Area'}), 400
946
+
947
+ else:
948
+ # For other types, compare as strings (case insensitive)
949
+ correct_answer = ground_truth[puzzle_id].get('answer')
950
+ is_correct = str(user_answer).lower() == str(correct_answer).lower()
951
+ correct_answer_info = correct_answer
952
+
953
+ # Get the appropriate answer field based on puzzle type
954
+ if puzzle_type == 'Dice_Count':
955
+ answer_key = 'sum'
956
+ elif puzzle_type == 'Patch_Select':
957
+ answer_key = 'correct_patches'
958
+ elif puzzle_type == 'Select_Animal':
959
+ answer_key = 'correct_patches'
960
+ elif puzzle_type == 'Coordinates':
961
+ answer_key = 'correct_option_index'
962
+ elif puzzle_type == 'Path_Finder':
963
+ answer_key = 'correct_option'
964
+ elif puzzle_type == 'Connect_icon':
965
+ answer_key = 'correct_option'
966
+ elif puzzle_type == 'Click_Order':
967
+ answer_key = 'answer'
968
+ elif puzzle_type == 'Hold_Button':
969
+ answer_key = 'hold_time'
970
+ elif puzzle_type == 'Misleading_Click':
971
+ answer_key = 'answer'
972
+ elif puzzle_type == 'Pick_Area':
973
+ answer_key = 'answer'
974
+ else:
975
+ answer_key = 'answer'
976
+
977
+ return jsonify({
978
+ 'correct': is_correct,
979
+ 'user_answer': user_answer,
980
+ 'correct_answer': ground_truth[puzzle_id].get(answer_key)
981
+ })
982
+
983
+ @app.route('/api/benchmark_results', methods=['POST'])
984
+ def record_benchmark():
985
+ data = request.json
986
+
987
+ # Add timestamp if not provided
988
+ if 'timestamp' not in data:
989
+ from datetime import datetime
990
+ data['timestamp'] = datetime.now().isoformat()
991
+
992
+ # In a real system, you would save this data to a database
993
+ # For this example, we'll just print it to the console
994
+ print(f"Benchmark results: {data}")
995
+
996
+ # You could store this in a log file as well
997
+ with open('benchmark_results.json', 'a') as f:
998
+ f.write(json.dumps(data) + '\n')
999
+
1000
+ return jsonify({'status': 'success'})
1001
+
1002
+ @app.route('/api/types', methods=['GET'])
1003
+ def get_types():
1004
+ """Get available CAPTCHA types"""
1005
+ return jsonify({
1006
+ 'types': get_captcha_types()
1007
+ })
1008
+
1009
+ if __name__ == '__main__':
1010
+ # For local development
1011
+ if os.environ.get('DEVELOPMENT'):
1012
+ app.run(debug=True)
1013
+ else:
1014
+ # For production on Hugging Face Spaces
1015
+ app.run(host='0.0.0.0', port=7860)
static/.DS_Store ADDED
Binary file (6.15 kB). View file
 
static/css/style.css ADDED
@@ -0,0 +1,1074 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ box-sizing: border-box;
3
+ margin: 0;
4
+ padding: 0;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
9
+ line-height: 1.6;
10
+ color: #333;
11
+ background-color: #f5f5f5;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1000px;
16
+ margin: 0 auto;
17
+ padding: 20px;
18
+ }
19
+
20
+ h1 {
21
+ text-align: center;
22
+ margin-bottom: 30px;
23
+ color: #2c3e50;
24
+ }
25
+
26
+ .benchmark-stats {
27
+ display: flex;
28
+ justify-content: center;
29
+ gap: 30px;
30
+ margin-bottom: 30px;
31
+ padding: 15px;
32
+ background-color: #fff;
33
+ border-radius: 8px;
34
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
35
+ }
36
+
37
+ .benchmark-stats span {
38
+ font-weight: 600;
39
+ }
40
+
41
+ .puzzle-container {
42
+ display: flex;
43
+ flex-direction: column;
44
+ gap: 20px;
45
+ background-color: white;
46
+ padding: 20px;
47
+ border-radius: 8px;
48
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
49
+ }
50
+
51
+ .puzzle-image-wrapper {
52
+ display: flex;
53
+ justify-content: center;
54
+ width: 100%;
55
+ }
56
+
57
+ .puzzle-image-container {
58
+ text-align: center;
59
+ position: relative;
60
+ display: inline-block;
61
+ margin: 0 auto 15px auto;
62
+ max-width: 100%;
63
+ }
64
+
65
+ #puzzle-image {
66
+ max-width: 100%;
67
+ max-height: 500px;
68
+ border-radius: 4px;
69
+ display: block;
70
+ }
71
+
72
+ #puzzle-image.clickable {
73
+ cursor: pointer;
74
+ border: 2px solid #3498db;
75
+ }
76
+
77
+ .puzzle-question {
78
+ text-align: center;
79
+ }
80
+
81
+ .puzzle-question h3 {
82
+ margin-bottom: 15px;
83
+ color: #2c3e50;
84
+ font-size: 18px;
85
+ }
86
+
87
+ .input-group {
88
+ display: flex;
89
+ justify-content: center;
90
+ gap: 10px;
91
+ margin-bottom: 15px;
92
+ }
93
+
94
+ #user-answer {
95
+ padding: 10px;
96
+ border: 1px solid #ddd;
97
+ border-radius: 4px;
98
+ font-size: 16px;
99
+ width: 200px;
100
+ }
101
+
102
+ #submit-answer {
103
+ padding: 10px 20px;
104
+ background-color: #2ecc71;
105
+ color: white;
106
+ border: none;
107
+ border-radius: 4px;
108
+ cursor: pointer;
109
+ font-size: 16px;
110
+ transition: background-color 0.3s;
111
+ }
112
+
113
+ #submit-answer:hover {
114
+ background-color: #27ae60;
115
+ }
116
+
117
+ #submit-answer:disabled {
118
+ background-color: #95a5a6;
119
+ cursor: not-allowed;
120
+ }
121
+
122
+ .result-message {
123
+ font-weight: 600;
124
+ margin-top: 10px;
125
+ min-height: 24px;
126
+ }
127
+
128
+ .result-message.correct {
129
+ color: #2ecc71;
130
+ }
131
+
132
+ .result-message.incorrect {
133
+ color: #e74c3c;
134
+ }
135
+
136
+ .result-message.instruction {
137
+ color: #3498db;
138
+ }
139
+
140
+ /* Click marker styles */
141
+ .click-marker {
142
+ position: absolute;
143
+ width: 20px;
144
+ height: 20px;
145
+ border-radius: 50%;
146
+ background-color: rgba(231, 76, 60, 0.7);
147
+ border: 2px solid #e74c3c;
148
+ transform: translate(-50%, -50%);
149
+ pointer-events: none;
150
+ /* Add animation */
151
+ animation: pulse 1s infinite alternate;
152
+ z-index: 10;
153
+ }
154
+
155
+ @keyframes pulse {
156
+ from {
157
+ transform: translate(-50%, -50%) scale(1);
158
+ box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4);
159
+ }
160
+ to {
161
+ transform: translate(-50%, -50%) scale(1.2);
162
+ box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
163
+ }
164
+ }
165
+
166
+ @media (max-width: 768px) {
167
+ .benchmark-stats {
168
+ flex-direction: column;
169
+ align-items: center;
170
+ gap: 10px;
171
+ }
172
+ }
173
+
174
+ /* Rotation CAPTCHA styles */
175
+ .rotation-layout {
176
+ display: flex;
177
+ justify-content: space-around;
178
+ align-items: center;
179
+ width: 100%;
180
+ margin-bottom: 15px;
181
+ }
182
+
183
+ .reference-image-container,
184
+ .object-image-container {
185
+ flex: 0 0 45%;
186
+ text-align: center;
187
+ position: relative;
188
+ }
189
+
190
+ .reference-image-container::before,
191
+ .object-image-container::before {
192
+ content: attr(data-label);
193
+ display: block;
194
+ font-size: 0.8rem;
195
+ color: #666;
196
+ margin-bottom: 5px;
197
+ }
198
+
199
+ .reference-image-container::before {
200
+ content: "Reference";
201
+ }
202
+
203
+ .object-image-container::before {
204
+ content: "Rotate to match";
205
+ }
206
+
207
+ #reference-image,
208
+ #object-image {
209
+ max-width: 100%;
210
+ max-height: 200px;
211
+ object-fit: contain;
212
+ }
213
+
214
+ /* Rotation controls */
215
+ .rotation-controls {
216
+ display: flex;
217
+ justify-content: center;
218
+ margin-top: 15px;
219
+ gap: 20px;
220
+ }
221
+
222
+ .rotate-left,
223
+ .rotate-right {
224
+ width: 40px;
225
+ height: 40px;
226
+ border-radius: 50%;
227
+ background-color: #2980b9;
228
+ color: white;
229
+ font-size: 20px;
230
+ border: none;
231
+ cursor: pointer;
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: center;
235
+ transition: background-color 0.2s;
236
+ }
237
+
238
+ .rotate-left:hover,
239
+ .rotate-right:hover {
240
+ background-color: #3498db;
241
+ }
242
+
243
+ .rotate-left:active,
244
+ .rotate-right:active {
245
+ background-color: #1c638d;
246
+ }
247
+
248
+ /* Submit button for rotation puzzles */
249
+ .rotation-submit {
250
+ text-align: center;
251
+ margin-top: 15px;
252
+ }
253
+
254
+ .submit-rotation {
255
+ background-color: #27ae60;
256
+ color: white;
257
+ border: none;
258
+ border-radius: 4px;
259
+ padding: 8px 16px;
260
+ font-size: 16px;
261
+ cursor: pointer;
262
+ transition: background-color 0.2s;
263
+ }
264
+
265
+ .submit-rotation:hover {
266
+ background-color: #2ecc71;
267
+ }
268
+
269
+ .submit-rotation:active {
270
+ background-color: #219653;
271
+ }
272
+
273
+ /* Ensure the puzzle container properly displays rotation puzzles */
274
+ .puzzle-image-wrapper {
275
+ width: 100%;
276
+ max-width: 600px;
277
+ margin: 0 auto;
278
+ }
279
+
280
+ /* Responsive adjustments */
281
+ @media (max-width: 768px) {
282
+ .rotation-layout {
283
+ flex-direction: column;
284
+ gap: 20px;
285
+ }
286
+
287
+ .reference-image-container,
288
+ .object-image-container {
289
+ flex: 0 0 100%;
290
+ }
291
+ }
292
+
293
+ /* Slider Puzzle Styles */
294
+ .background-container {
295
+ position: relative;
296
+ width: 100%;
297
+ max-width: 500px;
298
+ margin: 0 auto;
299
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
300
+ }
301
+
302
+ .slider-component {
303
+ position: absolute;
304
+ cursor: move;
305
+ user-select: none;
306
+ touch-action: none;
307
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
308
+ transition: box-shadow 0.2s ease;
309
+ }
310
+
311
+ .slider-component:hover {
312
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
313
+ }
314
+
315
+ .slider-component:active {
316
+ box-shadow: 0 5px 20px rgba(0, 0, 0, 0.5);
317
+ }
318
+
319
+ .slider-submit {
320
+ margin-top: 15px;
321
+ text-align: center;
322
+ }
323
+
324
+ .submit-slider {
325
+ background-color: #4CAF50;
326
+ color: white;
327
+ border: none;
328
+ padding: 8px 20px;
329
+ font-size: 16px;
330
+ cursor: pointer;
331
+ border-radius: 4px;
332
+ transition: background-color 0.3s ease;
333
+ }
334
+
335
+ .submit-slider:hover {
336
+ background-color: #45a049;
337
+ }
338
+
339
+ .submit-slider:disabled {
340
+ background-color: #cccccc;
341
+ cursor: not-allowed;
342
+ }
343
+
344
+ /* Hover instructions for the slider */
345
+ .background-container::before {
346
+ content: "Drag the puzzle piece to the correct position";
347
+ position: absolute;
348
+ top: -30px;
349
+ left: 50%;
350
+ transform: translateX(-50%);
351
+ padding: 5px 10px;
352
+ background-color: rgba(0, 0, 0, 0.7);
353
+ color: white;
354
+ border-radius: 4px;
355
+ font-size: 12px;
356
+ opacity: 0;
357
+ transition: opacity 0.3s ease;
358
+ pointer-events: none;
359
+ white-space: nowrap;
360
+ }
361
+
362
+ .background-container:hover::before {
363
+ opacity: 1;
364
+ }
365
+
366
+ /* Add responsive styles for smaller screens */
367
+ @media (max-width: 600px) {
368
+ .background-container {
369
+ max-width: 100%;
370
+ }
371
+
372
+ .slider-component img {
373
+ max-width: 100%;
374
+ }
375
+ }
376
+
377
+ /* Target area marker for slider puzzle debug */
378
+ .target-area {
379
+ position: absolute;
380
+ border-radius: 50%;
381
+ border: 2px dashed #00cc00;
382
+ background-color: rgba(0, 204, 0, 0.2);
383
+ pointer-events: none;
384
+ z-index: 5;
385
+ animation: pulse-green 1.5s infinite alternate;
386
+ }
387
+
388
+ @keyframes pulse-green {
389
+ from {
390
+ box-shadow: 0 0 0 0 rgba(0, 204, 0, 0.4);
391
+ background-color: rgba(0, 204, 0, 0.2);
392
+ }
393
+ to {
394
+ box-shadow: 0 0 0 10px rgba(0, 204, 0, 0);
395
+ background-color: rgba(0, 204, 0, 0.3);
396
+ }
397
+ }
398
+
399
+ /* Debugging coordinates label */
400
+ .coords-label {
401
+ position: absolute;
402
+ background-color: rgba(0, 0, 0, 0.7);
403
+ color: white;
404
+ padding: 2px 5px;
405
+ font-size: 10px;
406
+ border-radius: 3px;
407
+ white-space: nowrap;
408
+ z-index: 20;
409
+ }
410
+
411
+ /* Unusual Detection Grid Styles */
412
+ .unusual-detection-grid {
413
+ display: grid;
414
+ max-width: 600px;
415
+ margin: 0 auto;
416
+ background-color: #fff;
417
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
418
+ }
419
+
420
+ .grid-cell {
421
+ position: relative;
422
+ cursor: pointer;
423
+ border: 2px solid #333;
424
+ overflow: hidden;
425
+ transition: transform 0.2s ease;
426
+ display: flex;
427
+ justify-content: center;
428
+ align-items: center;
429
+ min-height: 80px;
430
+ }
431
+
432
+ .grid-cell:hover {
433
+ transform: scale(0.98);
434
+ }
435
+
436
+ .cell-image {
437
+ width: 100%;
438
+ height: 100%;
439
+ object-fit: cover;
440
+ display: block;
441
+ }
442
+
443
+ .cell-overlay {
444
+ position: absolute;
445
+ top: 0;
446
+ left: 0;
447
+ width: 100%;
448
+ height: 100%;
449
+ background-color: rgba(0, 120, 255, 0.5);
450
+ opacity: 0;
451
+ transition: opacity 0.2s ease;
452
+ pointer-events: none;
453
+ z-index: 2;
454
+ }
455
+
456
+ .checkmark {
457
+ position: absolute;
458
+ top: 50%;
459
+ left: 50%;
460
+ transform: translate(-50%, -50%);
461
+ color: white;
462
+ font-size: 32px;
463
+ font-weight: bold;
464
+ opacity: 0;
465
+ transition: opacity 0.2s ease;
466
+ pointer-events: none;
467
+ z-index: 3;
468
+ text-shadow: 1px 1px 3px rgba(0,0,0,0.7);
469
+ }
470
+
471
+ .submit-unusual {
472
+ margin-top: 15px;
473
+ padding: 10px 20px;
474
+ background-color: #4CAF50;
475
+ color: white;
476
+ border: none;
477
+ border-radius: 4px;
478
+ cursor: pointer;
479
+ font-size: 16px;
480
+ }
481
+
482
+ .submit-unusual:hover {
483
+ background-color: #45a049;
484
+ }
485
+
486
+ .submit-unusual:disabled {
487
+ background-color: #cccccc;
488
+ cursor: not-allowed;
489
+ }
490
+
491
+ /* End Unusual Detection Styles */
492
+
493
+ /* Image Matching Styles */
494
+ .matching-layout {
495
+ display: flex;
496
+ justify-content: space-around;
497
+ align-items: center;
498
+ width: 100%;
499
+ margin-bottom: 15px;
500
+ }
501
+
502
+ .option-image-container {
503
+ flex: 0 0 45%;
504
+ text-align: center;
505
+ position: relative;
506
+ }
507
+
508
+ .option-image-container::before {
509
+ content: "Match This";
510
+ }
511
+
512
+ #option-image {
513
+ max-width: 100%;
514
+ max-height: 200px;
515
+ object-fit: contain;
516
+ }
517
+
518
+ /* Navigation controls */
519
+ .image-matching-controls {
520
+ display: flex;
521
+ justify-content: center;
522
+ align-items: center;
523
+ margin-top: 15px;
524
+ gap: 20px;
525
+ }
526
+
527
+ .navigate-left,
528
+ .navigate-right {
529
+ width: 40px;
530
+ height: 40px;
531
+ border-radius: 50%;
532
+ background-color: #2980b9;
533
+ color: white;
534
+ font-size: 20px;
535
+ border: none;
536
+ cursor: pointer;
537
+ display: flex;
538
+ align-items: center;
539
+ justify-content: center;
540
+ transition: background-color 0.2s;
541
+ }
542
+
543
+ .navigate-left:hover,
544
+ .navigate-right:hover {
545
+ background-color: #3498db;
546
+ }
547
+
548
+ .navigate-left:active,
549
+ .navigate-right:active {
550
+ background-color: #1c638d;
551
+ }
552
+
553
+ /* Indicator dots */
554
+ .indicator-dots {
555
+ display: flex;
556
+ gap: 5px;
557
+ }
558
+
559
+ .dot {
560
+ width: 10px;
561
+ height: 10px;
562
+ border-radius: 50%;
563
+ background-color: #bdc3c7;
564
+ transition: background-color 0.2s;
565
+ }
566
+
567
+ .dot.active {
568
+ background-color: #2980b9;
569
+ }
570
+
571
+ /* Submit button for image matching */
572
+ .image-matching-submit {
573
+ text-align: center;
574
+ margin-top: 15px;
575
+ }
576
+
577
+ .submit-image-matching {
578
+ background-color: #27ae60;
579
+ color: white;
580
+ border: none;
581
+ border-radius: 4px;
582
+ padding: 8px 16px;
583
+ font-size: 16px;
584
+ cursor: pointer;
585
+ transition: background-color 0.2s;
586
+ }
587
+
588
+ .submit-image-matching:hover {
589
+ background-color: #2ecc71;
590
+ }
591
+
592
+ .submit-image-matching:active {
593
+ background-color: #219653;
594
+ }
595
+
596
+ /* Responsive adjustments */
597
+ @media (max-width: 768px) {
598
+ .matching-layout {
599
+ flex-direction: column;
600
+ gap: 20px;
601
+ }
602
+
603
+ .reference-image-container,
604
+ .option-image-container {
605
+ flex: 0 0 100%;
606
+ }
607
+ }
608
+
609
+ /* Add styles for Patch Select grid */
610
+ .patch-select-grid {
611
+ width: 100%;
612
+ max-width: 500px;
613
+ margin: 0 auto;
614
+ border: 2px solid #ccc;
615
+ overflow: hidden;
616
+ position: relative;
617
+ height: 450px; /* Set a fixed height */
618
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
619
+ }
620
+
621
+ .patch-select-image-container {
622
+ position: absolute;
623
+ top: 0;
624
+ left: 0;
625
+ width: 100%;
626
+ height: 100%;
627
+ z-index: 0;
628
+ }
629
+
630
+ .patch-select-image-container img {
631
+ object-fit: cover;
632
+ width: 100%;
633
+ height: 100%;
634
+ }
635
+
636
+ .patch-select-cell {
637
+ position: relative;
638
+ z-index: 1;
639
+ min-height: 80px; /* Increase minimum height */
640
+ cursor: pointer;
641
+ transition: all 0.2s ease;
642
+ border: 2px solid rgba(255, 255, 255, 0.5);
643
+ }
644
+
645
+ .patch-select-cell:hover {
646
+ background-color: rgba(0, 123, 255, 0.2);
647
+ border: 3px solid rgba(0, 123, 255, 0.7);
648
+ }
649
+
650
+ .patch-select-cell.selected {
651
+ background-color: rgba(0, 123, 255, 0.3);
652
+ border: 3px solid rgba(0, 123, 255, 0.8);
653
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.5);
654
+ }
655
+
656
+ /* Object Match Styles */
657
+ .object-match-container {
658
+ display: flex;
659
+ flex-direction: column;
660
+ align-items: center;
661
+ gap: 15px;
662
+ margin-bottom: 20px;
663
+ }
664
+
665
+ .object-match-horizontal-layout {
666
+ display: flex;
667
+ flex-direction: row;
668
+ justify-content: center;
669
+ align-items: center;
670
+ gap: 30px;
671
+ width: 100%;
672
+ }
673
+
674
+ .object-match-reference,
675
+ .object-match-options {
676
+ display: flex;
677
+ flex-direction: column;
678
+ align-items: center;
679
+ background-color: #f5f5f5;
680
+ border-radius: 8px;
681
+ padding: 15px;
682
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
683
+ width: 300px;
684
+ position: relative;
685
+ }
686
+
687
+ .object-match-reference-img,
688
+ .object-match-option-img {
689
+ max-width: 100%;
690
+ height: auto;
691
+ border-radius: 4px;
692
+ margin-bottom: 10px;
693
+ max-height: 200px;
694
+ object-fit: contain;
695
+ }
696
+
697
+ .object-match-caption {
698
+ font-weight: bold;
699
+ font-size: 16px;
700
+ margin-top: 5px;
701
+ }
702
+
703
+ .object-match-controls {
704
+ display: flex;
705
+ justify-content: space-between;
706
+ width: 100%;
707
+ margin-top: 10px;
708
+ }
709
+
710
+ .object-match-arrow {
711
+ background-color: #4CAF50;
712
+ color: white;
713
+ border: none;
714
+ border-radius: 50%;
715
+ width: 40px;
716
+ height: 40px;
717
+ font-size: 18px;
718
+ cursor: pointer;
719
+ display: flex;
720
+ align-items: center;
721
+ justify-content: center;
722
+ }
723
+
724
+ .object-match-arrow:hover {
725
+ background-color: #45a049;
726
+ }
727
+
728
+ .object-match-indicators {
729
+ display: flex;
730
+ justify-content: center;
731
+ gap: 8px;
732
+ margin-top: 15px;
733
+ }
734
+
735
+ .object-match-dot {
736
+ width: 10px;
737
+ height: 10px;
738
+ background-color: #ddd;
739
+ border-radius: 50%;
740
+ cursor: pointer;
741
+ }
742
+
743
+ .object-match-dot.active {
744
+ background-color: #4CAF50;
745
+ }
746
+
747
+ .object-match-submit {
748
+ padding: 10px 20px;
749
+ background-color: #4CAF50;
750
+ border: none;
751
+ color: white;
752
+ border-radius: 4px;
753
+ cursor: pointer;
754
+ font-size: 16px;
755
+ margin-top: 15px;
756
+ }
757
+
758
+ .object-match-submit:hover {
759
+ background-color: #45a049;
760
+ }
761
+
762
+ /* Responsive adjustments for Object Match */
763
+ @media (max-width: 768px) {
764
+ .object-match-horizontal-layout {
765
+ flex-direction: column;
766
+ gap: 20px;
767
+ }
768
+
769
+ .object-match-reference,
770
+ .object-match-options {
771
+ width: 100%;
772
+ }
773
+ }
774
+
775
+ /* Dart Count Styles */
776
+ .dart-count-container {
777
+ display: flex;
778
+ flex-direction: column;
779
+ align-items: center;
780
+ gap: 15px;
781
+ margin-bottom: 20px;
782
+ }
783
+
784
+ .dart-count-horizontal-layout {
785
+ display: flex;
786
+ flex-direction: row;
787
+ justify-content: center;
788
+ align-items: center;
789
+ gap: 30px;
790
+ width: 100%;
791
+ }
792
+
793
+ .dart-count-reference,
794
+ .dart-count-options {
795
+ display: flex;
796
+ flex-direction: column;
797
+ align-items: center;
798
+ background-color: #f5f5f5;
799
+ border-radius: 8px;
800
+ padding: 15px;
801
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
802
+ width: 300px;
803
+ position: relative;
804
+ }
805
+
806
+ .dart-count-reference-img,
807
+ .dart-count-option-img {
808
+ max-width: 100%;
809
+ height: auto;
810
+ border-radius: 4px;
811
+ margin-bottom: 10px;
812
+ max-height: 200px;
813
+ object-fit: contain;
814
+ }
815
+
816
+ .dart-count-caption {
817
+ font-weight: bold;
818
+ font-size: 16px;
819
+ margin-top: 5px;
820
+ }
821
+
822
+ .dart-count-controls {
823
+ display: flex;
824
+ justify-content: space-between;
825
+ width: 100%;
826
+ margin-top: 10px;
827
+ }
828
+
829
+ .dart-count-arrow {
830
+ background-color: #4CAF50;
831
+ color: white;
832
+ border: none;
833
+ border-radius: 50%;
834
+ width: 40px;
835
+ height: 40px;
836
+ font-size: 18px;
837
+ cursor: pointer;
838
+ display: flex;
839
+ align-items: center;
840
+ justify-content: center;
841
+ }
842
+
843
+ .dart-count-arrow:hover {
844
+ background-color: #45a049;
845
+ }
846
+
847
+ .dart-count-indicators {
848
+ display: flex;
849
+ justify-content: center;
850
+ gap: 8px;
851
+ margin-top: 15px;
852
+ }
853
+
854
+ .dart-count-dot {
855
+ width: 10px;
856
+ height: 10px;
857
+ background-color: #ddd;
858
+ border-radius: 50%;
859
+ cursor: pointer;
860
+ }
861
+
862
+ .dart-count-dot.active {
863
+ background-color: #4CAF50;
864
+ }
865
+
866
+ .dart-count-submit {
867
+ padding: 10px 20px;
868
+ background-color: #4CAF50;
869
+ border: none;
870
+ color: white;
871
+ border-radius: 4px;
872
+ cursor: pointer;
873
+ font-size: 16px;
874
+ margin-top: 15px;
875
+ }
876
+
877
+ .dart-count-submit:hover {
878
+ background-color: #45a049;
879
+ }
880
+
881
+ /* Responsive adjustments for Dart Count */
882
+ @media (max-width: 768px) {
883
+ .dart-count-horizontal-layout {
884
+ flex-direction: column;
885
+ gap: 20px;
886
+ }
887
+
888
+ .dart-count-reference,
889
+ .dart-count-options {
890
+ width: 100%;
891
+ }
892
+ }
893
+
894
+ /* Difficulty stars styling */
895
+ .difficulty-stars {
896
+ display: inline-block;
897
+ margin-left: 10px;
898
+ margin-bottom: 5px;
899
+ }
900
+
901
+ .difficulty-stars .star {
902
+ color: #FFD700; /* Yellow color for stars */
903
+ font-size: 24px; /* Increased size */
904
+ margin-right: 5px; /* Increased spacing */
905
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); /* Added shadow for better visibility */
906
+ }
907
+
908
+ .puzzle-difficulty {
909
+ display: flex;
910
+ align-items: center;
911
+ justify-content: center;
912
+ margin-top: 5px;
913
+ margin-bottom: 15px;
914
+ font-size: 16px; /* Increased text size */
915
+ font-weight: bold; /* Made text bold */
916
+ color: #333;
917
+ background-color: #f8f8f8; /* Light background */
918
+ padding: 10px;
919
+ border-radius: 5px;
920
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
921
+ max-width: 300px;
922
+ margin-left: auto;
923
+ margin-right: auto;
924
+ }
925
+
926
+ .puzzle-difficulty span {
927
+ margin-right: 5px;
928
+ }
929
+
930
+ /* === Special Effects for Answer Results === */
931
+
932
+ /* Fireworks container */
933
+ .fireworks-container {
934
+ position: fixed;
935
+ top: 0;
936
+ left: 0;
937
+ width: 100%;
938
+ height: 100%;
939
+ pointer-events: none;
940
+ z-index: 9999;
941
+ overflow: hidden;
942
+ }
943
+
944
+ /* Individual firework particles */
945
+ .firework {
946
+ position: absolute;
947
+ width: 8px;
948
+ height: 8px;
949
+ border-radius: 50%;
950
+ animation: explode 1.5s forwards;
951
+ opacity: 0;
952
+ box-shadow: 0 0 10px 2px rgba(255, 255, 255, 0.7);
953
+ }
954
+
955
+ @keyframes explode {
956
+ 0% {
957
+ transform: translateY(0) scale(0.1);
958
+ opacity: 0;
959
+ }
960
+ 25% {
961
+ opacity: 1;
962
+ }
963
+ 50% {
964
+ opacity: 0.8;
965
+ }
966
+ 100% {
967
+ transform: translateY(-250px) scale(1.2);
968
+ opacity: 0;
969
+ }
970
+ }
971
+
972
+ /* Happy face animation */
973
+ .happy-face-container {
974
+ position: fixed;
975
+ top: 50%;
976
+ left: 50%;
977
+ transform: translate(-50%, -50%);
978
+ z-index: 9999;
979
+ pointer-events: none;
980
+ font-size: 180px;
981
+ animation: happy-face-animation 2.5s forwards;
982
+ opacity: 0;
983
+ text-shadow: 0 0 20px rgba(255, 215, 0, 0.8);
984
+ }
985
+
986
+ @keyframes happy-face-animation {
987
+ 0% {
988
+ opacity: 0;
989
+ transform: translate(-50%, -50%) scale(0.1) rotate(-20deg);
990
+ }
991
+ 20% {
992
+ opacity: 1;
993
+ transform: translate(-50%, -50%) scale(1.3) rotate(10deg);
994
+ }
995
+ 50% {
996
+ transform: translate(-50%, -50%) scale(1.2) rotate(-5deg);
997
+ }
998
+ 80% {
999
+ opacity: 1;
1000
+ transform: translate(-50%, -50%) scale(1.3) rotate(0deg);
1001
+ }
1002
+ 100% {
1003
+ opacity: 0;
1004
+ transform: translate(-50%, -50%) scale(0.8);
1005
+ }
1006
+ }
1007
+
1008
+ /* Sad face animation */
1009
+ .sad-face-container {
1010
+ position: fixed;
1011
+ top: 50%;
1012
+ left: 50%;
1013
+ transform: translate(-50%, -50%);
1014
+ z-index: 9999;
1015
+ pointer-events: none;
1016
+ font-size: 150px;
1017
+ animation: sad-face-animation 2s forwards;
1018
+ opacity: 0;
1019
+ }
1020
+
1021
+ @keyframes sad-face-animation {
1022
+ 0% {
1023
+ opacity: 0;
1024
+ transform: translate(-50%, -50%) scale(0.1);
1025
+ }
1026
+ 50% {
1027
+ opacity: 1;
1028
+ transform: translate(-50%, -50%) scale(1.2);
1029
+ }
1030
+ 100% {
1031
+ opacity: 0;
1032
+ transform: translate(-50%, -50%) scale(0.8);
1033
+ }
1034
+ }
1035
+
1036
+ /* Bounce animation for correct message */
1037
+ .result-message.correct {
1038
+ color: #2ecc71;
1039
+ animation: bounce 0.5s;
1040
+ font-size: 1.5em;
1041
+ text-shadow: 0 0 5px rgba(46, 204, 113, 0.5);
1042
+ }
1043
+
1044
+ @keyframes bounce {
1045
+ 0%, 20%, 50%, 80%, 100% {
1046
+ transform: translateY(0);
1047
+ }
1048
+ 40% {
1049
+ transform: translateY(-20px);
1050
+ }
1051
+ 60% {
1052
+ transform: translateY(-10px);
1053
+ }
1054
+ }
1055
+
1056
+ /* Shake animation for incorrect message */
1057
+ .result-message.incorrect {
1058
+ color: #e74c3c;
1059
+ animation: shake 0.5s;
1060
+ font-size: 1.5em;
1061
+ text-shadow: 0 0 5px rgba(231, 76, 60, 0.5);
1062
+ }
1063
+
1064
+ @keyframes shake {
1065
+ 0%, 100% {
1066
+ transform: translateX(0);
1067
+ }
1068
+ 10%, 30%, 50%, 70%, 90% {
1069
+ transform: translateX(-5px);
1070
+ }
1071
+ 20%, 40%, 60%, 80% {
1072
+ transform: translateX(5px);
1073
+ }
1074
+ }
static/js/script.js ADDED
The diff for this file is too large to render. See raw diff
 
templates/index.html ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>CAPTCHA Puzzle Benchmark</title>
7
+ <link rel="stylesheet" href="/static/css/style.css">
8
+ <style>
9
+ /* Additional styling to ensure input field visibility */
10
+ .input-group {
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ margin: 15px 0;
15
+ width: 100%;
16
+ }
17
+ #user-answer {
18
+ flex: 1;
19
+ padding: 10px;
20
+ border: 2px solid #ccc;
21
+ border-radius: 4px;
22
+ margin-right: 10px;
23
+ font-size: 16px;
24
+ }
25
+ #submit-answer {
26
+ padding: 10px 20px;
27
+ background-color: #4CAF50;
28
+ border: none;
29
+ color: white;
30
+ border-radius: 4px;
31
+ cursor: pointer;
32
+ font-size: 16px;
33
+ transition: background-color 0.3s ease;
34
+ }
35
+ #submit-answer:hover {
36
+ background-color: #45a049;
37
+ }
38
+ #submit-answer:disabled {
39
+ background-color: #cccccc;
40
+ cursor: not-allowed;
41
+ }
42
+ </style>
43
+ </head>
44
+ <body>
45
+ <div class="container">
46
+ <h1>Open CaptchaWorld</h1>
47
+ <div id="debug-indicator" style="color: #ff5722; margin-top: -15px; margin-bottom: 10px; font-weight: bold; display: none;">
48
+ Debugging Mode: <span id="debug-type-display"></span>
49
+ </div>
50
+
51
+ <div class="benchmark-stats">
52
+ <span>Total: <span id="total-count">0</span></span>
53
+ <span>Correct: <span id="correct-count">0</span></span>
54
+ <span>Accuracy: <span id="accuracy">0%</span></span>
55
+ </div>
56
+ <div class="puzzle-difficulty" style="margin-bottom: 15px;">
57
+ <span>Reasoning Depth:</span>
58
+ <div class="difficulty-stars" id="difficulty-stars"></div>
59
+ </div>
60
+ <div class="puzzle-container" id="puzzle-container">
61
+ <div class="puzzle-image-wrapper">
62
+ <div class="puzzle-image-container">
63
+ <img id="puzzle-image" src="" alt="CAPTCHA Puzzle">
64
+ </div>
65
+ </div>
66
+ <div class="puzzle-question">
67
+ <h3 id="puzzle-prompt">Loading puzzle...</h3>
68
+ <div class="input-group">
69
+ <input type="text" id="user-answer" placeholder="Your answer">
70
+ <button id="submit-answer">Submit</button>
71
+ </div>
72
+ <div id="result-message" class="result-message"></div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Inline script for immediate star display -->
78
+ <script>
79
+ // Display placeholder stars immediately before main script loads
80
+ window.onload = function() {
81
+ const starsContainer = document.getElementById('difficulty-stars');
82
+ if (starsContainer) {
83
+ starsContainer.innerHTML = '';
84
+ for (let i = 0; i < 5; i++) {
85
+ const star = document.createElement('span');
86
+ star.className = 'star';
87
+ star.innerHTML = i < 3 ? '★' : '☆'; // Default to 3 stars
88
+ starsContainer.appendChild(star);
89
+ }
90
+ }
91
+ };
92
+ </script>
93
+
94
+ <script src="/static/js/script.js"></script>
95
+ </body>
96
+ </html>