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]