Phases d'exécution : timelines jsPsych, tests de casque et tâches externes
Exécutez une timeline jsPsych 8, un test de casque noté par le serveur, ou une tâche oTree ou un questionnaire hébergé avec retours signés, comme une page d'une étude Potato.
Nouveau dans la v2.12.0
Une phase d'exécution est une page d'une étude Potato qui exécute une tâche au lieu d'afficher une page de questions. Potato sert la page, transmet à la tâche l'identifiant du participant, ses conditions et les réponses de questionnaire choisies, enregistre ce que la tâche consigne, et fait avancer le participant quand elle se termine. Le consentement, les questionnaires, l'annotation, le retour vers Prolific ou SONA et l'export restent dans Potato.
Toute phase d'exécution :
- occupe l'emplacement prestudy ou poststudy, déterminé par sa place dans
phases.orderpar rapport àannotation(ou fixé avecslot:), et accepteshow_when,exit_whenet le contrebalancement comme n'importe quelle autre page de phase (Déroulement de l'expérience) ; - s'exécute sous une Content-Security-Policy qui n'autorise les scripts et les requêtes réseau que vers le serveur Potato ;
- ne peut pas être sautée, car tant que les données de la tâche ne sont pas enregistrées, le serveur refuse le formulaire de la phase et le Next de la page d'annotation ;
- enregistre ses données dans la base de données du projet (
project.sqlite, tableruntime_runs) au lieu deuser_state.json, et les exporte avec--format runtime, ou--format jspsychpour les seules pages jsPsych.
Ces clés s'appliquent à toute phase d'exécution :
| Clé | Valeur par défaut | Signification |
|---|---|---|
extra_scripts | [] | Fichiers .js supplémentaires du répertoire de la tâche, chargés avant la tâche |
assets_dir | aucune | Un dossier de fichiers que la tâche peut charger |
expose_answers | [] | Réponses à des questions de questionnaire antérieures transmises à la tâche |
save | on_finish | incremental enregistre aussi chaque essai dès qu'il se termine |
allow_rerun | false | Indique si revenir sur une page terminée relance la tâche |
max_data_mb | 10 | Taille maximale d'une exécution acceptée par le serveur |
csp.extra_sources | [] | Origines https:// depuis lesquelles la page peut charger des polices, des images ou des scripts |
Pages jsPsych
phases:
order: [consent, stroop, annotation, debrief]
stroop:
type: jspsych
timeline: tasks/stroop.js
plugins: [instructions, html-keyboard-response]Potato embarque jsPsych 8.3.0 et 15 de ses plugins ; une étude qui n'utilise que ceux-ci ne charge donc rien depuis un CDN et fonctionne sur un serveur sans accès à Internet :
html-keyboard-response, html-button-response, html-slider-response, image-keyboard-response, image-button-response, audio-keyboard-response, audio-button-response, video-keyboard-response, instructions, preload, fullscreen, survey-likert, survey-text, survey-multi-choice et call-function.
Les autres plugins, y compris ceux de jspsych-contrib, se placent dans extra_scripts sous forme de builds pour navigateur, c'est-à-dire le fichier index.browser.min.js du paquet npm.
| Clé | Valeur par défaut | Signification |
|---|---|---|
timeline | obligatoire | Un fichier .js du répertoire de la tâche qui définit window.potatoTimeline |
plugins | [] | Plugins à charger, parmi la liste ci-dessus |
extensions | [] | Extensions jsPsych à charger et à transmettre à initJsPsych. Potato embarque mouse-tracking |
slot | déduit | prestudy avant la phase d'annotation, poststudy après. Un emplacement explicite qui contredit la place de la page dans order l'emporte, et un avertissement est journalisé |
L'exit_when d'une page jsPsych est évalué à la fin de la timeline ; les données de l'exécution sont donc déjà enregistrées pour un participant qu'il exclut. Pour exclure des personnes avant qu'elles ne fassent la tâche, placez le questionnaire de sélection sur sa propre page, devant elle.
Le fichier de timeline
window.potatoTimeline = function (jsPsych, potato) {
var trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p style="font-size:48px">RED</p>',
choices: ['r', 'g', 'b']
};
return [trial];
};window.potatoTimeline est un tableau de timeline, ou une fonction qui en renvoie un. Potato appelle la fonction avec l'instance jsPsych qu'il a créée et un objet potato :
| Propriété | Contenu |
|---|---|
potato.participant | Le nom d'utilisateur Potato, par exemple l'identifiant Prolific ou SONA |
potato.conditions | {factor: level} issu de conditions |
potato.answers | {question: answer} pour les questions de expose_answers. Les réponses arrivent sous forme de chaînes, nombres compris ("30") |
potato.attempt | 1 à la première exécution, davantage dès qu'un rechargement suit une exécution qui avait envoyé des données |
potato.asset(path) | L'URL d'un fichier de assets_dir |
potato.finish() | Termine la tâche immédiatement et enregistre ce qui a été recueilli |
N'appelez pas initJsPsych vous-même et ne définissez pas on_finish sur l'expérience, car Potato fait les deux. Le on_finish au niveau des essais et le reste de l'API jsPsych fonctionnent comme d'habitude. Chaque essai reçoit potato_user, potato_page, potato_run_id, potato_attempt et une colonne condition_<factor> par facteur.
Les stimuli se chargent par potato.asset(), et le plugin preload les précharge comme dans toute expérience jsPsych :
{
type: jsPsychImageKeyboardResponse,
stimulus: potato.asset('faces/f01.png')
}Suivi de la souris
extensions: [mouse-tracking] charge @jspsych/extension-mouse-tracking 1.2.0 et l'enregistre. Un essai l'active de la manière habituelle, et chacun de ces essais reçoit mouse_tracking_data, une liste d'échantillons {x, y, t, event}, ainsi que mouse_tracking_targets :
{
type: jsPsychHtmlButtonResponse,
stimulus: '<p id="stim">Which is bigger?</p>',
choices: ['3', '8'],
extensions: [{type: jsPsychExtensionMouseTracking, params: {targets: ['#stim']}}]
}L'extension d'oculométrie WebGazer n'est pas fournie. WebGazer a besoin de 'unsafe-eval', que la Content-Security-Policy de la page n'autorise pas.
À la fin de la timeline
La page envoie les données au serveur, affiche « Saving your responses » et passe à la page suivante une fois que le serveur les a enregistrées. Si l'enregistrement échoue, elle réessaie puis affiche un bouton Try again, et le participant n'avance pas sans ses données.
Un rechargement relance la timeline depuis le début, car jsPsych ne peut pas reprendre une timeline en cours de route. Avec la valeur par défaut save: on_finish, les essais effectués avant un rechargement sont perdus, et seul le compteur page_loads de la page dans participants.csv montre que c'est arrivé. Avec save: incremental, une exécution qui a envoyé des données est conservée comme incomplète et un rechargement lance une nouvelle tentative ; une analyse peut alors exclure les deuxièmes tentatives ou les garder. Ouvrir la tâche dans un deuxième onglet compte comme un rechargement, et l'exécution du premier onglet ne peut plus se terminer.
Test de casque
Une page headphone_check vérifie qu'un participant à une étude d'écoute porte un casque et n'écoute pas sur des haut-parleurs.
phases:
order: [consent, headphones, annotation]
headphones:
type: headphone_check
test: huggins
trials: 6
pass_threshold: 5| Clé | Valeur par défaut | Signification |
|---|---|---|
test | huggins | huggins, antiphase ou both |
trials | 6 | Essais par test, de 2 à 20 |
pass_threshold | 5 | Essais corrects nécessaires pour réussir, par test |
on_fail | screen_out | screen_out met fin à l'étude pour un participant qui échoue. continue enregistre le résultat et poursuit |
message | un texte par défaut | Ce que lit un participant exclu |
La page demande au participant de mettre son casque et de régler un volume confortable, puis lance les essais. Chaque essai joue trois sons une seule fois, et le participant en choisit un.
huggins(Milne et al., 2021) joue trois salves de bruit. Dans l'une d'elles, une bande étroite autour de 600 Hz est inversée en phase dans une oreille, ce qui s'entend comme un son faible au casque et pas sur des haut-parleurs. Le participant répond à « Which sound had a faint tone hidden in the noise? »antiphase(Woods et al., 2017) joue trois sons purs de 200 Hz. L'un est plus faible de 6 dB, et un autre est en opposition de phase entre les oreilles, ce qui l'atténue en partie sur des haut-parleurs et le fait paraître le plus faible dans ce cas. Le participant répond à « Which sound was the quietest? »
Le serveur produit les sons quand la page les demande ; aucun fichier audio n'est donc livré avec Potato. Il choisit aussi lequel des trois sons est la bonne réponse, et envoie les trois sons de chaque essai dans un seul WAV qui n'indique pas lequel c'est. Le navigateur n'envoie que le choix. Une valeur de cible ou de « correct » dans ce qu'il envoie est ignorée, tout comme une deuxième réponse au même essai et une réponse à un son qui n'a jamais été récupéré ; des réponses inventées obtiennent donc un score de niveau hasard.
La page répond à deux questions que les pages suivantes peuvent tester : <page>_passed (yes ou no) et <page>_correct (un nombre, ou <page>_correct_huggins et <page>_correct_antiphase pour test: both). Avec on_fail: continue, testez la réponse vous-même :
phases:
speakers_note:
type: prestudy
file: surveys/speakers.json
show_when:
- {schema: headphones_passed, operator: equals, value: "no"}Réussir le test ne prouve pas que le participant portait un casque. Une personne sur haut-parleurs peut deviner juste une fois sur trois à chaque essai, et un participant qui écrit du code pour analyser le son téléchargé peut trouver la réponse, comme avec tout test de casque dont les sons parviennent au navigateur.
Tâches externes
Une page type: external envoie le participant vers une tâche sur un autre serveur et le ramène quand elle se termine. La tâche s'exécute sur l'autre serveur ; Potato transmet le participant et enregistre son retour.
phases:
order: [consent, game, annotation]
game:
type: external
url: "https://otree.example.org/room/public_goods/?participant_label={participant}&potato_return={return_url}"
return_params: [payoff]
description: The next part is a short game with three other participants.
button_text: Start the game| Clé | Valeur par défaut | Signification |
|---|---|---|
url | obligatoire | Où envoyer le participant, avec des espaces réservés. Elle doit être en https://, ou en http pour localhost, 127.0.0.1 ou [::1] |
return_params | [] | Paramètres de requête à conserver depuis le lien de retour |
return_secret | aucune | Une chaîne privée d'au moins 16 caractères, partagée avec l'autre système. Quand elle est définie, le retour doit être signé |
description | aucune | Texte au-dessus du bouton |
button_text | « Continue to the task » | Le libellé du bouton |
new_tab | false | Ouvre la tâche dans un nouvel onglet |
Les espaces réservés de url sont encodés pour URL : {participant} (l'identifiant utilisateur Potato), {return_url} (l'adresse qui ramène le participant), {run_id} et {condition.<factor>}.
L'autre système envoie le participant vers {return_url} quand il a terminé, en ajoutant les résultats éventuels comme paramètres de requête, par exemple <return_url>&payoff=4.50. Le retour doit arriver dans la même session de navigateur et désigner la tentative en cours du participant ; un lien périmé affiche « This link is out of date » et ne change rien. La page répond ensuite <page>_returned (yes) et <page>_<param> pour chaque paramètre listé, que les pages suivantes peuvent tester, et les paramètres sont enregistrés avec l'exécution.
Retours signés
Le lien de retour figure dans l'adresse vers laquelle le participant est envoyé ; un participant peut donc le lire, remplacer payoff=4.50 par payoff=99 et l'ouvrir sans jouer. Sans return_secret, les paramètres retournés sont déclaratifs, et ils ne devraient pas déterminer un paiement à moins de les vérifier par rapport aux enregistrements de l'autre système.
Avec return_secret, l'autre système ajoute sig au lien de retour : le HMAC-SHA256 en hexadécimal, avec le secret pour clé, de l'identifiant d'exécution suivi de &<param>=<value> pour chaque paramètre de return_params, triés par nom. Chaque nom et chaque valeur est encodé en pourcentage avec tous les caractères réservés échappés, de sorte qu'une valeur contenant & ou = ne peut pas se faire passer pour un autre paramètre. En Python :
import hashlib, hmac
from urllib.parse import quote
message = run_id + "".join(f"&{quote(k, safe='')}={quote(params[k], safe='')}"
for k in sorted(params))
sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
# redirect to f"{return_url}&{urlencode({**params, 'sig': sig})}"Un retour sans sig, ou avec une signature qui ne correspond pas, reçoit le statut 409 et la page « This return could not be confirmed ». La page n'est pas marquée comme terminée ; un retour correctement signé fonctionne donc encore ensuite. Le secret doit rester sur le serveur de l'autre système. Un questionnaire hébergé qui construit sa redirection finale dans le navigateur du participant ne peut pas le garder privé ; la signature n'y sert donc à rien.
- oTree : envoyez les participants vers une URL de salle avec
participant_label={participant}. L'application doit transporter elle-même l'URL de retour : lisez le paramètrepotato_returnà l'arrivée du participant, conservez-le dansparticipant.vars, et redirigez vers lui depuis la dernière page. La façon dont la première page lit les paramètres d'URL dépend de la version d'oTree ; consultez donc sa documentation. - Empirica : transmettez
{participant}comme paramètre d'URLparticipantKey. - Un questionnaire ou une expérience hébergés (Qualtrics, Gorilla, Pavlovia) : placez
{participant}dans une donnée intégrée ou un paramètre d'URL, et définissez la redirection finale vers l'URL de retour.
Données et export
python -m potato.export --config config.yaml --format runtime --output ./export/L'export écrit ces fichiers pour chaque type d'exécution utilisé par le projet, où <kind> vaut jspsych, headphone_check ou external :
| Fichier | Contenu |
|---|---|
<kind>_runs.csv | Une ligne par exécution : participant, page, tentative, statut, nombre d'essais et conditions |
<kind>_trials.csv | Une ligne par essai, avec chaque colonne enregistrée par la tâche. Les valeurs imbriquées sont en JSON |
<kind>_interaction.csv | Événements de perte de focus, de focus et de plein écran, quand il y en a eu |
<kind>_raw/<user>/<page>-<run>.json | Les données de chaque exécution telles que la tâche les a produites |
Les exécutions qui ne se sont pas terminées ont le statut started ou partial. Un essai de test de casque enregistre test, trial, index (sa position dans l'exécution) et response (le son choisi, de 1 à 3), plus trois valeurs ajoutées par le serveur : target (le bon son), correct, et scored, qui vaut false pour une réponse que le serveur n'a pas comptée.
Sécurité et chronométrage
La timeline est du code que vous avez écrit, et elle s'exécute sur l'origine du serveur Potato avec la session du participant, avec le même niveau de confiance qu'une page de consignes HTML personnalisée. Sa Content-Security-Policy n'autorise les scripts, les styles et les médias que depuis le serveur Potato et les éventuelles csp.extra_sources, et les requêtes réseau que vers le serveur Potato ; un stimulus qui affiche du texte de participant ne peut donc ni charger depuis autre chose ni y envoyer quoi que ce soit. Échappez tout de même ce texte avant de le placer dans du HTML.
Chaque fichier nommé par la page doit se trouver dans le répertoire de la tâche. La route des ressources ne sert que des types de fichiers de stimuli, jamais de .js ni de .html depuis assets_dir, et envoie chacun avec sa propre CSP de sandbox ; un SVG ouvert directement n'exécute donc aucun script. csp.extra_sources accepte des origines https:// uniques et refuse les caractères génériques.
Le chronométrage propre à jsPsych s'applique sur la page, car les scripts d'annotation de Potato n'y sont pas chargés. La documentation de jsPsych sur la précision temporelle indique que les temps de réponse mesurés en JavaScript tendent à être de 10 à 40 ms plus longs que dans les logiciels de laboratoire, avec une variance comparable.
Exemples
python potato/flask_server.py start examples/advanced/jspsych-stroop/config.yaml -p 8000
python potato/flask_server.py start examples/audio/listening-test-p808/config.yaml -p 8000Le premier est une tâche de Stroop placée entre un questionnaire de sélection et une tâche de notation de mots, avec un retour montré à la moitié des participants. Le second est un test de casque suivi d'extraits de parole notés sur l'échelle ACR.
Pour aller plus loin
- Déroulement de l'expérience - Conditions,
show_when,exit_whenet contrebalancement - Venir de jsPsych - Les concepts de jsPsych mis en correspondance avec les clés de configuration de Potato
- Tests d'écoute - MUSHRA, ACR et les autres méthodes de test d'écoute
- Essais chronométrés - Réponses rapides sur les items d'annotation sans timeline jsPsych
Pour les détails d'implémentation, consultez la documentation source sur les phases d'exécution et la phase jsPsych.