from otree.api import * import boto3 from botocore.config import Config as BotoConfig import base64 import os import random import pandas as pd from dotenv import load_dotenv load_dotenv() doc = """ Voice actor recording study — participants read statements aloud in different tonalities """ CONDITION_INSTRUCTIONS = { 'neutral': ( "Please read the following expression in a neutral but natural tone, " "without conveying any preference between the options." ), 'weak': ( "Please read the following expression in a way that conveys that you prefer " "Option A slightly more than Option B, and that the difference between both options " "is small." ), 'strong': ( "Please read the following expression in a way that conveys that you prefer " "Option A much more than Option B, and that Option A " "stands out clearly compared to Option B." ), } CONDITION_LABELS = { 'neutral': 'Neutral', 'weak': 'Slight Preference', 'strong': 'Strong Preference', } _df = pd.read_csv('_static/global/statements.csv', sep=',').apply( lambda col: col.str.strip() if col.dtype == 'object' else col ) _STIMULI = _df.to_dict('records') class C(BaseConstants): NAME_IN_URL = 'voice' PLAYERS_PER_GROUP = None NUM_STATEMENTS = len(_STIMULI) NUM_ROUNDS = NUM_STATEMENTS * 3 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): base64 = models.LongStringField(blank=True) statement_id = models.IntegerField(blank=True) condition = models.StringField(blank=True) screen_width = models.IntegerField(blank=True) screen_height = models.IntegerField(blank=True) touch_capable = models.IntegerField(initial=0) def creating_session(subsession): if subsession.round_number == 1: for player in subsession.get_players(): rng = random.Random(player.participant.id_in_session) # Randomize statement order and assign one variant per statement statements = list(_STIMULI) rng.shuffle(statements) statement_trials = [] for stmt in statements: statement_trials.append({ 'statement_id': int(stmt['statement_id']), 'text': stmt['text'], }) # Weak/strong order randomized once per participant pref_order = ['weak', 'strong'] if rng.random() < 0.5 else ['strong', 'weak'] # Phase 1: all 9 neutrals trials = [{**st, 'condition': 'neutral'} for st in statement_trials] # Phase 2: same statement order, blocked weak+strong (or strong+weak) for st in statement_trials: for condition in pref_order: trials.append({**st, 'condition': condition}) player.participant.vars['trials'] = trials class record(Page): form_model = 'player' form_fields = ['screen_width', 'screen_height', 'touch_capable'] @staticmethod def vars_for_template(player: Player): trial = player.participant.vars['trials'][player.round_number - 1] player.statement_id = trial['statement_id'] player.condition = trial['condition'] return dict( instruction=CONDITION_INSTRUCTIONS[trial['condition']], condition=trial['condition'], condition_label=CONDITION_LABELS[trial['condition']], statement=trial['text'], round_number=player.round_number, total_rounds=C.NUM_ROUNDS, progress_pct=round(player.round_number / C.NUM_ROUNDS * 100), ) @staticmethod async def live_method(player: Player, data): if 'text' in data: b64 = base64.b64decode(data['text']) player.base64 = data['text'] filename = ( f"{player.session.code}_{player.participant.code}" f"_{player.statement_id}_{player.condition}.wav" ) print(filename) try: s3_client = boto3.client( 's3', aws_access_key_id=os.environ.get('S3_ACCESS_KEY'), aws_secret_access_key=os.environ.get('S3_SECRET_KEY'), region_name='eu-north-1', config=BotoConfig(signature_version='s3v4'), ) s3_client.put_object( Bucket='ethz-otree-whisper', Key=filename, Body=b64, ) yield {player.id_in_group: {"status": "uploaded"}} except Exception as e: print(f"S3 upload error: {e}") yield {player.id_in_group: {"status": "error"}} class transition(Page): @staticmethod def is_displayed(player: Player): # Show once, right before the first preference round return player.round_number == len(_STIMULI) + 1 page_sequence = [transition, record]