from otree.api import * import copy, random, itertools, string from io import BytesIO import base64 from PIL import Image, ImageDraw, ImageFont, ImageFilter from matplotlib import font_manager doc = """ Your app description """ def create_falling_digits_gif(digits, output_path="captcha.gif", frame_count=40, image_size=(200, 100)): frames = [] font = ImageFont.truetype("arial.ttf", 32) # You can use another .ttf if arial not found # Create falling effect by gradually increasing y-position for frame_idx in range(frame_count): img = Image.new("RGB", image_size, color="white") draw = ImageDraw.Draw(img) for i, digit in enumerate(digits): x = 30 + i * 30 y = int((frame_idx - i * 7) * 30) # stagger fall by index if y > image_size[1]: continue draw.text((x, y), digit, font=font, fill="black") frames.append(img) # Save as animated gif frames[0].save(output_path, format='GIF', append_images=frames[1:], save_all=True, duration=100, loop=0) print(f"Saved CAPTCHA gif as {output_path}") class C(BaseConstants): NAME_IN_URL = 'Captcha' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass def creating_session(subsession): if subsession.round_number == 1: for p in subsession.get_players(): gif = random.choice(['6204', '5548', '1599', '0417', '9754']) p.participant.vars['gif'] = gif class Group(BaseGroup): pass class Player(BasePlayer): captcha_solution = models.StringField() captcha_input = models.StringField() captcha_fail = models.IntegerField(initial=1) consent = models.IntegerField( label='''I have read and consent to the above''', choices=[[1, 'Yes, continue with the survey'], [0, 'No, exit the survey']], widget=widgets.RadioSelectHorizontal ) P_id = models.StringField( label='''Please enter your Prolific ID. Please make sure it is correct. Otherwise we will not be able to pay to you.''', ) captcha_solution = models.StringField() # PAGES class Consent(Page): form_model = 'player' form_fields = ['consent'] def is_displayed(player: Player): return player.round_number == 1 def before_next_page(player: Player, timeout_happened): player.participant.vars['consent'] = player.consent class NoConsent(Page): def is_displayed(player: Player): return player.participant.vars['consent'] == 0 class Captcha(Page): form_model = 'player' form_fields = ['captcha_input'] def before_next_page(player: Player, timeout_happened): # Check if the CAPTCHA was answered correctly correct_captcha = player.participant.vars.get('captcha_text', '').lower() if player.captcha_input.strip().lower() == correct_captcha: player.captcha_fail = 0 # Set to 1 if correct else: player.captcha_fail = 1 # Set to 0 if incorrect # if 'FailCount' in player.participant.vars: # player.participant.vars['FailCount'] = 'FailCount' + player.captcha_fail # else: # player.participant.vars['FailCount'] = 0 if 'Nfail' in player.participant.vars: player.participant.vars['Nfail'] = player.participant.vars['Nfail'] + player.captcha_fail else: player.participant.vars['Nfail'] = player.captcha_fail def vars_for_template(player): # Generate random CAPTCHA char_set = string.ascii_uppercase.replace('O', '') + string.digits.replace('0', '') # Generate the CAPTCHA text captcha_text = ''.join(random.choices(char_set, k=6)) player.participant.vars['captcha_text'] = captcha_text # Create CAPTCHA image width, height = 220, 80 img = Image.new('RGB', (width, height), color=(255, 255, 255)) draw = ImageDraw.Draw(img) # Dynamically find an available font available_fonts = font_manager.findSystemFonts(fontpaths=None, fontext='ttf') good_fonts = [f for f in available_fonts if any(keyword in f.lower() for keyword in ["arial", "sans", "dejavu", "verdana", "tahoma", "helvetica", "times"])] chosen_font = good_fonts[0] if good_fonts else available_fonts[0] if available_fonts else None try: font = ImageFont.truetype(chosen_font, 40) if chosen_font else ImageFont.load_default() except IOError: font = ImageFont.load_default() # Draw CAPTCHA text spacing = 30 for i, char in enumerate(captcha_text): x = 20 + i * spacing y = random.randint(15, 25) temp_img = Image.new('RGBA', (40, 50), (255, 255, 255, 0)) temp_draw = ImageDraw.Draw(temp_img) temp_draw.text((5, 5), char, font=font, fill=(0, 0, 0)) temp_img = temp_img.rotate(random.randint(-15, 15), expand=True) img.paste(temp_img, (x, y), temp_img) # Background noise for _ in range(150): x, y = random.randint(0, width), random.randint(0, height) draw.point((x, y), fill=(random.randint(100, 255), random.randint(100, 255), random.randint(100, 255))) for _ in range(5): x1, y1, x2, y2 = [random.randint(0, dim) for dim in (width, height, width, height)] draw.line((x1, y1, x2, y2), fill=(random.randint(50, 200), random.randint(50, 200), random.randint(50, 200)), width=2) # Apply distortion and blur img = img.transform((width, height), Image.AFFINE, (1, random.uniform(-0.11, 0.11), 0, random.uniform(-0.13, 0.13), 1, 0)) img = img.filter(ImageFilter.GaussianBlur(0.9)) # Convert image to base64 buffer = BytesIO() img.save(buffer, format="PNG") captcha_image = base64.b64encode(buffer.getvalue()).decode() return {"captcha_image": captcha_image} class CaptchaGif(Page): form_model = 'player' form_fields = ['captcha_input'] timeout_seconds = 30 def vars_for_template(player): digits = [str(random.randint(0, 9)) for _ in range(4)] code = ''.join(digits) player.captcha_solution = code # # Your fixed output path # base_static_path = r"C:\Users\Moritz\Dropbox\oTree\PfR_feedback\_static" # gif_filename = f"{code}.gif" # gif_path = os.path.join(base_static_path, gif_filename) # # # Generate GIF only if it doesn't already exist # if not os.path.exists(gif_path): # create_falling_digits_gif(digits, gif_path) gif = player.participant.vars['gif'] + ".gif" return dict(captcha_filename=gif) def before_next_page(player: Player, timeout_happened): if player.captcha_input == player.participant.vars['gif']: player.participant.vars['Nfail'] = 0 else: player.participant.vars['Nfail'] = 3 class ProlificID(Page): def is_displayed(self): return self.round_number == 1 form_model = 'player' form_fields = ['P_id'] class kicked(Page): def is_displayed(player): return player.participant.vars['Nfail'] >= 2 # page_sequence = [Consent, NoConsent, ProlificID, Captcha, kicked ] page_sequence = [Consent, NoConsent, CaptchaGif, kicked ] # page_sequence = [CaptchaGif, kicked]