From 0e7ade45a2e564aa748be4b0d4dad96bcbfc9f61 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 27 Aug 2024 13:12:41 +0200 Subject: [PATCH 1/8] fix speech2song: use session.get_rounds_passed instead of session.current_round --- backend/experiment/rules/speech2song.py | 73 +++++++++++++------------ 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py index 36ace5332..756ff0f2e 100644 --- a/backend/experiment/rules/speech2song.py +++ b/backend/experiment/rules/speech2song.py @@ -1,16 +1,13 @@ import random from django.utils.translation import gettext as _ -from django.template.loader import render_to_string from .base import Base -from experiment.actions import Consent, Explainer, Step, Final, Playlist, Trial +from experiment.actions import Explainer, Step, Final, Trial from experiment.actions.form import Form, RadiosQuestion from experiment.actions.playback import Autoplay -from question.demographics import EXTRA_DEMOGRAPHICS -from question.languages import LANGUAGE, LanguageQuestion -from question.utils import question_by_key +from question.languages import LanguageQuestion from session.models import Session @@ -57,7 +54,7 @@ def get_intro_explainer(self): button_label=_('Start') ) - def next_round(self, session): + def next_round(self, session: Session): blocks = [1, 2, 3] # shuffle blocks based on session.id as seed -> always same order for same session random.seed(session.id) @@ -65,31 +62,39 @@ def next_round(self, session): # group_ids for practice (0), or one of the speech blocks (1-3) actions = [] is_speech = True - if session.current_round == 1: + rounds_passed = session.get_rounds_passed(self.counted_result_keys) + print(rounds_passed) + if rounds_passed == 0: question_trials = self.get_open_questions(session) if question_trials: + session.save_json_data({'quesionnaire': True}) return [self.get_intro_explainer(), *question_trials] - - explainer = Explainer( - instruction=_( - 'Thank you for answering these questions about your background!'), - steps=[ - Step( - description=_( - 'Now you will hear a sound repeated multiple times.') - ), - Step( - description=_( - 'Please listen to the following segment carefully, if possible with headphones.') - ), - ], - button_label=_('OK') - ) - return [ - explainer, - *next_repeated_representation(session, is_speech, 0) - ] - if session.current_round == 2: + elif session.load_json_data().get('questionnaire'): + explainer = Explainer( + instruction=_( + 'Thank you for answering these questions about your background!'), + steps=[ + Step( + description=_( + 'Now you will hear a sound repeated multiple times.') + ), + Step( + description=_( + 'Please listen to the following segment carefully, if possible with headphones.') + ), + ], + button_label=_('OK') + ) + return [ + explainer, + *next_repeated_representation(session, is_speech, 0) + ] + else: + return [ + self.get_intro_explainer(), + *next_repeated_representation(session, is_speech, 0) + ] + elif rounds_passed == 1: e1 = Explainer( instruction=_('Previous studies have shown that many people perceive the segment you just heard as song-like after repetition, but it is no problem if you do not share that perception because there is a wide range of individual differences.'), steps=[], @@ -107,13 +112,13 @@ def next_round(self, session): ) actions.extend([e1, e2]) group_id = blocks[0] - elif 2 < session.current_round <= n_rounds_per_block + 1: + elif 2 <= rounds_passed <= n_rounds_per_block: group_id = blocks[0] - elif n_rounds_per_block + 1 < session.current_round <= 2 * n_rounds_per_block + 1: + elif n_rounds_per_block < rounds_passed <= 2 * n_rounds_per_block: group_id = blocks[1] - elif 2 * n_rounds_per_block + 1 < session.current_round <= 3 * n_rounds_per_block + 1: + elif 2 * n_rounds_per_block < rounds_passed <= 3 * n_rounds_per_block: group_id = blocks[2] - elif session.current_round == 3 * n_rounds_per_block + 2: + elif rounds_passed == 3 * n_rounds_per_block: # Final block (environmental sounds) e3 = Explainer( instruction=_('Part2'), @@ -134,7 +139,7 @@ def next_round(self, session): actions.append(e3) group_id = 4 is_speech = False - elif 3 * n_rounds_per_block + 2 < session.current_round <= 4 * n_rounds_per_block + 1: + elif 3 * n_rounds_per_block < rounds_passed <= 4 * n_rounds_per_block: group_id = 4 is_speech = False else: @@ -148,7 +153,7 @@ def next_round(self, session): final_text=_( 'Thank you for contributing your time to science!') ) - if session.current_round % 2 == 0: + if rounds_passed % 2 == 1: # even round: single representation (first round are questions only) actions.extend(next_single_representation( session, is_speech, group_id)) From c6bfb75865bf36ba84cdedfdd108399daf03ecd4 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 2 Sep 2024 16:18:02 +0200 Subject: [PATCH 2/8] fix: remove playlist view from musical_preferences --- backend/experiment/rules/musical_preferences.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index afba3f8a8..517b34447 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -80,7 +80,6 @@ def next_round(self, session: Session): ) return [explainer, *question_trials] else: - playlist = Playlist(session.block.playlists.all()) explainer = Explainer( instruction=_("How to play"), steps=[ @@ -96,7 +95,7 @@ def next_round(self, session: Session): ], button_label=_("Start") ) - actions = [playlist, explainer] + actions = [explainer] else: if last_result.question_key == 'audio_check1': playback = get_test_playback() From c463bd033c7f9790bb680892d229d029d3d7b1dd Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 2 Sep 2024 16:18:21 +0200 Subject: [PATCH 3/8] fix: add playlist action to huang_2022 --- backend/experiment/rules/huang_2022.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/experiment/rules/huang_2022.py b/backend/experiment/rules/huang_2022.py index b0d49dd9b..c2e196e9f 100644 --- a/backend/experiment/rules/huang_2022.py +++ b/backend/experiment/rules/huang_2022.py @@ -136,7 +136,8 @@ def next_round(self, session: Session): step_numbers=True, button_label=_("Continue") ) - actions.extend([explainer, explainer_devices, *self.next_song_sync_action(session, round_number)]) + playlist = Playlist(session.block.playlists.all()) + actions.extend([explainer, explainer_devices, playlist, *self.next_song_sync_action(session, round_number)]) else: # Load the heard_before offset. heard_before_offset = session.load_json_data().get('heard_before_offset') From 3cce809aae11833a741684db855e2c743fec782f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 2 Sep 2024 16:32:24 +0200 Subject: [PATCH 4/8] fix musical preferences: adjust round counter --- backend/experiment/rules/musical_preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index 517b34447..691a2be73 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -208,7 +208,7 @@ def next_round(self, session: Session): playback=playback, feedback_form=form, title=_('Song %(round)s/%(total)s') % { - 'round': round_number, 'total': session.block.rounds}, + 'round': round_number + 1, 'total': session.block.rounds}, config={ 'response_time': section.duration + .1, } From 9acd5873ed6018a81228e5ce8b2fb8b4e1d5f423 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 2 Sep 2024 16:35:28 +0200 Subject: [PATCH 5/8] refactor musical preferences: increment preference_offset on top of file --- backend/experiment/rules/musical_preferences.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index 691a2be73..dba625a8d 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -21,7 +21,7 @@ class MusicalPreferences(Base): ID = 'MUSICAL_PREFERENCES' default_consent_file = 'consent/consent_musical_preferences.html' - preference_offset = 20 + preference_offset = 21 knowledge_offset = 42 contact_email = 'musicexp_china@163.com' counted_result_keys = ['like_song'] @@ -130,7 +130,7 @@ def next_round(self, session: Session): return [self.get_intro_explainer(), Trial(playback=playback, feedback_form=form, html=html, config={'response_time': 15}, title=_("Audio check"))] - if round_number == self.preference_offset + 1: + if round_number == self.preference_offset: like_results = session.result_set.filter(question_key='like_song') feedback = Trial( html=HTML(body=render_to_string('html/musical_preferences/feedback.html', { From a641c3e1f9e0b6eab29edad827035beacb5d6b0c Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 3 Sep 2024 06:56:44 +0200 Subject: [PATCH 6/8] fix condition for showing explainer of block 4 --- backend/experiment/rules/speech2song.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py index 756ff0f2e..1239a5a52 100644 --- a/backend/experiment/rules/speech2song.py +++ b/backend/experiment/rules/speech2song.py @@ -63,7 +63,6 @@ def next_round(self, session: Session): actions = [] is_speech = True rounds_passed = session.get_rounds_passed(self.counted_result_keys) - print(rounds_passed) if rounds_passed == 0: question_trials = self.get_open_questions(session) if question_trials: @@ -118,7 +117,7 @@ def next_round(self, session: Session): group_id = blocks[1] elif 2 * n_rounds_per_block < rounds_passed <= 3 * n_rounds_per_block: group_id = blocks[2] - elif rounds_passed == 3 * n_rounds_per_block: + elif rounds_passed == 3 * n_rounds_per_block + 1: # Final block (environmental sounds) e3 = Explainer( instruction=_('Part2'), From 371ecfa4cddd68ee6f2d6b7f4c151d963bc21916 Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Tue, 3 Sep 2024 09:18:04 +0200 Subject: [PATCH 7/8] add speech2song runthrough unit test --- backend/experiment/rules/speech2song.py | 80 +++++++++---------- .../rules/tests/test_speech2song.py | 37 +++++++-- 2 files changed, 72 insertions(+), 45 deletions(-) diff --git a/backend/experiment/rules/speech2song.py b/backend/experiment/rules/speech2song.py index 1239a5a52..5ecb17fbd 100644 --- a/backend/experiment/rules/speech2song.py +++ b/backend/experiment/rules/speech2song.py @@ -13,16 +13,16 @@ from result.utils import prepare_result -n_representations = 8 -n_trials_per_block = 8 -n_rounds_per_block = n_trials_per_block * 2 # each trial has two rounds - class Speech2Song(Base): """ Rules for a speech-to-song experiment """ ID = 'SPEECH_TO_SONG' default_consent_file = 'consent/consent_speech2song.html' + n_presentations = 8 + n_trials_per_block = 8 + n_rounds_per_trial = 2 + def __init__(self): self.question_series = [ { @@ -86,12 +86,12 @@ def next_round(self, session: Session): ) return [ explainer, - *next_repeated_representation(session, is_speech, 0) + *self.next_repeated_representation(session, is_speech, 0) ] else: return [ self.get_intro_explainer(), - *next_repeated_representation(session, is_speech, 0) + *self.next_repeated_representation(session, is_speech, 0) ] elif rounds_passed == 1: e1 = Explainer( @@ -111,13 +111,13 @@ def next_round(self, session: Session): ) actions.extend([e1, e2]) group_id = blocks[0] - elif 2 <= rounds_passed <= n_rounds_per_block: + elif 2 <= rounds_passed <= self.n_trials_per_block * self.n_rounds_per_trial: group_id = blocks[0] - elif n_rounds_per_block < rounds_passed <= 2 * n_rounds_per_block: + elif self.n_trials_per_block * self.n_rounds_per_trial < rounds_passed <= 2 * self.n_trials_per_block * self.n_rounds_per_trial: group_id = blocks[1] - elif 2 * n_rounds_per_block < rounds_passed <= 3 * n_rounds_per_block: + elif 2 * self.n_trials_per_block * self.n_rounds_per_trial < rounds_passed <= 3 * self.n_trials_per_block * self.n_rounds_per_trial: group_id = blocks[2] - elif rounds_passed == 3 * n_rounds_per_block + 1: + elif rounds_passed == 3 * self.n_trials_per_block * self.n_rounds_per_trial + 1: # Final block (environmental sounds) e3 = Explainer( instruction=_('Part2'), @@ -138,7 +138,7 @@ def next_round(self, session: Session): actions.append(e3) group_id = 4 is_speech = False - elif 3 * n_rounds_per_block < rounds_passed <= 4 * n_rounds_per_block: + elif 3 * self.n_trials_per_block * self.n_rounds_per_trial < rounds_passed <= 4 * self.n_trials_per_block * self.n_rounds_per_trial: group_id = 4 is_speech = False else: @@ -154,42 +154,42 @@ def next_round(self, session: Session): ) if rounds_passed % 2 == 1: # even round: single representation (first round are questions only) - actions.extend(next_single_representation( + actions.extend(self.next_single_representation( session, is_speech, group_id)) else: # uneven round: repeated representation - actions.extend(next_repeated_representation( + actions.extend(self.next_repeated_representation( session, is_speech)) return actions -def next_single_representation(session: Session, is_speech: bool, group_id: int) -> list: - """ combine a question after the first representation, - and several repeated representations of the sound, - with a final question""" - filter_by = {'group': group_id} - section = session.playlist.get_section(filter_by, song_ids=session.get_unused_song_ids()) - actions = [sound(section), speech_or_sound_question(session, section, is_speech)] - return actions - - -def next_repeated_representation(session: Session, is_speech: bool, group_id: int = -1) -> list: - if group_id == 0: - # for the Test case, there is no previous section to look at - section = session.playlist.section_set.get(group=group_id) - else: - section = session.previous_section() - actions = [sound(section)] * n_representations - actions.append(speech_or_sound_question(session, section, is_speech)) - return actions - - -def speech_or_sound_question(session, section, is_speech) -> Trial: - if is_speech: - question = question_speech(session, section) - else: - question = question_sound(session, section) - return Trial(playback=None, feedback_form=Form([question])) + def next_single_representation(self, session: Session, is_speech: bool, group_id: int) -> list: + """ combine a question after the first representation, + and several repeated representations of the sound, + with a final question""" + filter_by = {'group': group_id} + section = session.playlist.get_section(filter_by, song_ids=session.get_unused_song_ids()) + actions = [sound(section), self.speech_or_sound_question(session, section, is_speech)] + return actions + + + def next_repeated_representation(self, session: Session, is_speech: bool, group_id: int = -1) -> list: + if group_id == 0: + # for the Test case, there is no previous section to look at + section = session.playlist.section_set.get(group=group_id) + else: + section = session.previous_section() + actions = [sound(section)] * self.n_presentations + actions.append(self.speech_or_sound_question(session, section, is_speech)) + return actions + + + def speech_or_sound_question(self, session, section, is_speech) -> Trial: + if is_speech: + question = question_speech(session, section) + else: + question = question_sound(session, section) + return Trial(playback=None, feedback_form=Form([question])) def question_speech(session, section): diff --git a/backend/experiment/rules/tests/test_speech2song.py b/backend/experiment/rules/tests/test_speech2song.py index 397ef86a2..4601129ed 100644 --- a/backend/experiment/rules/tests/test_speech2song.py +++ b/backend/experiment/rules/tests/test_speech2song.py @@ -6,8 +6,8 @@ from section.models import Playlist from session.models import Session -from experiment.actions import Trial -from experiment.rules.speech2song import next_repeated_representation, next_single_representation, sound, Speech2Song +from experiment.actions import Explainer, Final, Trial +from experiment.rules.speech2song import sound, Speech2Song from experiment.serializers import serialize_actions @@ -30,7 +30,7 @@ def setUpTestData(cls): cls.playlist.update_sections() cls.participant = Participant.objects.create() cls.block = Block.objects.create( - rules='SPEECH_TO_SONG', slug='s2s', rounds=42) + rules='SPEECH_TO_SONG', slug='s2s', rounds=16) cls.session = Session.objects.create( block=cls.block, participant=cls.participant, @@ -44,7 +44,7 @@ def test_sound_method(self): def test_single_presentation(self): group = self.playlist.section_set.first().group - actions = next_single_representation(self.session, True, int(group)) + actions = self.session.block_rules().next_single_representation(self.session, True, int(group)) self.assertEqual(type(actions), list) def test_repeated_presentation(self): @@ -55,7 +55,7 @@ def test_repeated_presentation(self): section=section, score=2 ) - actions = next_repeated_representation(self.session, True) + actions = self.session.block_rules().next_repeated_representation(self.session, True) self.assertEqual(type(actions), list) def test_next_round(self): @@ -70,3 +70,30 @@ def test_next_round_serialization(self): self.assertEqual(type(serialized), list) for s in serialized: self.assertEqual(type(s), dict) + + def test_runthrough(self): + speech2song = self.session.block_rules() + speech2song.n_trials_per_block = 2 + for i in range(self.block.rounds - 1): + actions = speech2song.next_round(self.session) + feedback_trial = next((a for a in actions if a.__dict__.get('feedback_form'))) + result = Result.objects.get(pk=feedback_trial.feedback_form.form[0].result_id) + result.score = 42 + result.save() + if i == 0: + self.assertEqual(self._number_of_sound_trials(actions), speech2song.n_presentations) + elif i == 1: + self.assertIsInstance(actions[0], Explainer) + self.assertEqual(self._number_of_sound_trials(actions), 1) + elif i == self.block.rounds - 1: + self.assertIsInstance(actions[0], Final) + elif i == speech2song.n_trials_per_block * 2 * 3 + 1: + self.assertIsInstance(actions[0], Explainer) + elif i % 2 == 0: + self.assertEqual(self._number_of_sound_trials(actions), speech2song.n_presentations) + elif i % 2 == 1: + self.assertEqual(self._number_of_sound_trials(actions), 1) + + def _number_of_sound_trials(self, actions): + sound_actions = [a for a in actions if a.__dict__.get('playback')] + return len(sound_actions) From aa0b2261f78120a177496dc767d42bfdaddb2e0f Mon Sep 17 00:00:00 2001 From: BeritJanssen Date: Mon, 9 Sep 2024 11:13:37 +0200 Subject: [PATCH 8/8] fix: add comment of preference / knowledge offset --- backend/experiment/rules/musical_preferences.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/experiment/rules/musical_preferences.py b/backend/experiment/rules/musical_preferences.py index dba625a8d..3c9b61961 100644 --- a/backend/experiment/rules/musical_preferences.py +++ b/backend/experiment/rules/musical_preferences.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from django.template.loader import render_to_string -from experiment.actions import Explainer, Final, HTML, Playlist, Redirect, Step, Trial +from experiment.actions import Explainer, Final, HTML, Redirect, Step, Trial from experiment.actions.form import BooleanQuestion, ChoiceQuestion, Form, LikertQuestionIcon from experiment.actions.playback import Autoplay from experiment.actions.styles import STYLE_BOOLEAN, STYLE_BOOLEAN_NEGATIVE_FIRST @@ -19,10 +19,15 @@ class MusicalPreferences(Base): + ''' This rules file presents repeated trials with a combined form: + participants are asked to state how much they like the song, and whether they know the song + after 21 and 42 rounds, participants see summaries of their choices, + and at the final round, participants see other participants' preferred songs + ''' ID = 'MUSICAL_PREFERENCES' default_consent_file = 'consent/consent_musical_preferences.html' - preference_offset = 21 - knowledge_offset = 42 + preference_offset = 21 # after this many rounds rounds, show information with the participant's preferences + knowledge_offset = 42 # after this many rounds, show additionally how many songs the participant knows contact_email = 'musicexp_china@163.com' counted_result_keys = ['like_song']