Fases de ejecución: líneas de tiempo de jsPsych, comprobaciones de auriculares y tareas externas
Ejecuta una línea de tiempo de jsPsych 8, una comprobación de auriculares puntuada en el servidor o una tarea de oTree o de una encuesta alojada con retornos firmados como una página de un estudio de Potato.
Nuevo en v2.12.0
Una fase de ejecución es una página de un estudio de Potato que ejecuta una tarea en lugar de mostrar una página de preguntas. Potato sirve la página, pasa a la tarea el ID del participante, sus condiciones y las respuestas de encuesta elegidas, guarda lo que la tarea registra y hace avanzar al participante cuando termina. El consentimiento, las encuestas, la anotación, el traspaso a Prolific o SONA y la exportación siguen en Potato.
Toda fase de ejecución:
- ocupa la posición prestudy o poststudy, que se decide por su lugar en
phases.orderrespecto aannotation(o se fija conslot:), y admiteshow_when,exit_wheny contrabalanceo como cualquier otra página de fase (Flujo del experimento); - se ejecuta bajo una Content-Security-Policy que permite scripts y peticiones de red solo hacia el servidor de Potato;
- no se puede saltar, porque hasta que se guardan los datos de la tarea el servidor rechaza el formulario de la fase y el botón Next de la página de anotación;
- guarda sus datos en la base de datos del proyecto (
project.sqlite, tablaruntime_runs) en lugar de enuser_state.json, y los exporta con--format runtime, o con--format jspsychsolo para las páginas de jsPsych.
Estas claves se aplican a toda fase de ejecución:
| Clave | Valor predeterminado | Significado |
|---|---|---|
extra_scripts | [] | Más archivos .js del directorio de la tarea, cargados antes que la tarea |
assets_dir | ninguno | Una carpeta de archivos que la tarea puede cargar |
expose_answers | [] | Respuestas a preguntas de encuesta anteriores que se pasan a la tarea |
save | on_finish | incremental guarda además cada ensayo en cuanto termina |
allow_rerun | false | Si volver a una página terminada ejecuta la tarea de nuevo |
max_data_mb | 10 | La ejecución más grande que acepta el servidor |
csp.extra_sources | [] | Orígenes https:// desde los que la página puede cargar fuentes, imágenes o scripts |
Páginas de jsPsych
phases:
order: [consent, stroop, annotation, debrief]
stroop:
type: jspsych
timeline: tasks/stroop.js
plugins: [instructions, html-keyboard-response]Potato incluye jsPsych 8.3.0 y 15 de sus plugins, así que un estudio que use solo esos no carga nada desde una CDN y funciona en un servidor sin acceso a 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 y call-function.
Los demás plugins, incluidos los de jspsych-contrib, van en extra_scripts como compilaciones para navegador, es decir, el archivo index.browser.min.js del paquete npm.
| Clave | Valor predeterminado | Significado |
|---|---|---|
timeline | obligatorio | Un archivo .js del directorio de la tarea que define window.potatoTimeline |
plugins | [] | Plugins que cargar, de la lista anterior |
extensions | [] | Extensiones de jsPsych que cargar y pasar a initJsPsych. Potato incluye mouse-tracking |
slot | inferido | prestudy antes de la fase de anotación, poststudy después. Una posición explícita que contradice el lugar de la página en order prevalece, y se registra una advertencia |
El exit_when de una página de jsPsych se comprueba cuando termina la línea de tiempo, así que los datos de la ejecución ya están guardados para el participante al que excluye. Para excluir a las personas antes de que hagan la tarea, pon el cuestionario de selección en su propia página delante de ella.
El archivo de la línea de tiempo
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 es un array de línea de tiempo, o una función que devuelve uno. Potato llama a la función con la instancia de jsPsych que ha creado y un objeto potato:
| Propiedad | Contenido |
|---|---|
potato.participant | El nombre de usuario de Potato, como el ID de Prolific o SONA |
potato.conditions | {factor: level} a partir de conditions |
potato.answers | {question: answer} para las preguntas de expose_answers. Las respuestas llegan como cadenas, también los números ("30") |
potato.attempt | 1 en la primera ejecución, un valor mayor cuando una recarga sigue a una ejecución que ya había enviado datos |
potato.asset(path) | La URL de un archivo de assets_dir |
potato.finish() | Termina la tarea en ese momento y guarda lo recogido |
No llames tú a initJsPsych ni definas on_finish en el experimento, porque Potato hace ambas cosas. El on_finish a nivel de ensayo y el resto de la API de jsPsych funcionan como siempre. Cada ensayo recibe potato_user, potato_page, potato_run_id, potato_attempt y una columna condition_<factor> por cada factor.
Los estímulos se cargan mediante potato.asset(), y el plugin preload los precarga como en cualquier experimento de jsPsych:
{
type: jsPsychImageKeyboardResponse,
stimulus: potato.asset('faces/f01.png')
}Seguimiento del ratón
extensions: [mouse-tracking] carga @jspsych/extension-mouse-tracking 1.2.0 y lo registra. Un ensayo lo activa de la forma habitual, y cada uno de esos ensayos recibe mouse_tracking_data, una lista de muestras {x, y, t, event}, y mouse_tracking_targets:
{
type: jsPsychHtmlButtonResponse,
stimulus: '<p id="stim">Which is bigger?</p>',
choices: ['3', '8'],
extensions: [{type: jsPsychExtensionMouseTracking, params: {targets: ['#stim']}}]
}La extensión de seguimiento ocular WebGazer no se incluye. WebGazer necesita 'unsafe-eval', que la Content-Security-Policy de la página no permite.
Cuando termina la línea de tiempo
La página envía los datos al servidor, muestra "Saving your responses" y pasa a la página siguiente en cuanto el servidor los ha guardado. Si el guardado falla, lo reintenta y después muestra un botón Try again, y el participante no avanza sin sus datos.
Una recarga reinicia la línea de tiempo desde el principio, porque jsPsych no puede reanudar una línea de tiempo a medias. Con el valor predeterminado save: on_finish, los ensayos hechos antes de una recarga se pierden, y solo el recuento page_loads de la página en participants.csv muestra que ocurrió. Con save: incremental, una ejecución que ha enviado datos se conserva como incompleta y una recarga inicia un nuevo intento, de modo que un análisis puede excluir los segundos intentos o conservarlos. Abrir la tarea en una segunda pestaña cuenta como una recarga, y la ejecución de la primera pestaña ya no puede terminar.
Comprobación de auriculares
Una página headphone_check comprueba que un participante de un estudio de escucha lleva auriculares y no escucha por altavoces.
phases:
order: [consent, headphones, annotation]
headphones:
type: headphone_check
test: huggins
trials: 6
pass_threshold: 5| Clave | Valor predeterminado | Significado |
|---|---|---|
test | huggins | huggins, antiphase o both |
trials | 6 | Ensayos por prueba, de 2 a 20 |
pass_threshold | 5 | Ensayos correctos necesarios para aprobar, por prueba |
on_fail | screen_out | screen_out termina el estudio para un participante que no aprueba. continue registra el resultado y sigue adelante |
message | uno predeterminado | Lo que lee un participante excluido |
La página pide al participante que se ponga los auriculares y ajuste un volumen cómodo, y después ejecuta los ensayos. Cada ensayo reproduce tres sonidos una sola vez, y el participante elige uno.
huggins(Milne et al., 2021) reproduce tres ráfagas de ruido. En una de ellas, una banda estrecha alrededor de 600 Hz tiene la fase invertida en un oído, lo que con auriculares se oye como un tono tenue y con altavoces no. El participante responde a "Which sound had a faint tone hidden in the noise?"antiphase(Woods et al., 2017) reproduce tres tonos de 200 Hz. Uno es 6 dB más bajo, y otro está en antifase entre los oídos, lo que con altavoces se cancela en parte y ahí suena como el más bajo. El participante responde a "Which sound was the quietest?"
El servidor genera los sonidos cuando la página los pide, así que Potato no incluye archivos de audio. También elige cuál de los tres sonidos es la respuesta, y envía los tres sonidos de cada ensayo como un único WAV que no indica cuál es. El navegador envía solo la elección. Un valor de objetivo o de "correcto" en lo que envía se ignora, igual que una segunda respuesta al mismo ensayo y una respuesta a un sonido que nunca se descargó, así que las respuestas inventadas aciertan al nivel del azar.
La página responde dos preguntas que las páginas posteriores pueden comprobar: <page>_passed (yes o no) y <page>_correct (un número, o <page>_correct_huggins y <page>_correct_antiphase con test: both). Con on_fail: continue, comprueba tú mismo la respuesta:
phases:
speakers_note:
type: prestudy
file: surveys/speakers.json
show_when:
- {schema: headphones_passed, operator: equals, value: "no"}Aprobar no demuestra que el participante llevara auriculares. Alguien con altavoces puede acertar por azar una vez de cada tres en cada ensayo, y un participante que escriba código para analizar el sonido descargado puede encontrar la respuesta, como ocurre con cualquier prueba de auriculares cuyos sonidos llegan al navegador.
Tareas externas
Una página type: external envía al participante a una tarea en otro servidor y lo trae de vuelta cuando termina. La tarea se ejecuta en el otro servidor, y Potato le traspasa el participante y registra que ha vuelto.
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| Clave | Valor predeterminado | Significado |
|---|---|---|
url | obligatorio | Adónde enviar al participante, con marcadores de posición. Tiene que ser https://, o http para localhost, 127.0.0.1 o [::1] |
return_params | [] | Parámetros de consulta que conservar del enlace de retorno |
return_secret | ninguno | Una cadena privada de al menos 16 caracteres, compartida con el otro sistema. Si se define, el retorno debe estar firmado |
description | ninguno | Texto encima del botón |
button_text | "Continue to the task" | La etiqueta del botón |
new_tab | false | Abre la tarea en una pestaña nueva |
Los marcadores de posición de url se codifican para URL: {participant} (el ID de usuario de Potato), {return_url} (la dirección que trae de vuelta al participante), {run_id} y {condition.<factor>}.
El otro sistema envía al participante a {return_url} cuando termina, y añade los resultados como parámetros de consulta, como en <return_url>&payoff=4.50. El retorno debe llegar en la misma sesión del navegador y nombrar el intento actual del participante, y un enlace caducado muestra "This link is out of date" y no cambia nada. La página responde entonces <page>_returned (yes) y <page>_<param> para cada parámetro de la lista, que las páginas posteriores pueden comprobar, y los parámetros se guardan con la ejecución.
Retornos firmados
El enlace de retorno está en la dirección a la que se envía al participante, así que un participante puede leerlo, cambiar payoff=4.50 por payoff=99 y abrirlo sin jugar. Sin return_secret, los parámetros devueltos son autodeclarados, y no deberían decidir un pago salvo que los contrastes con los registros del otro sistema.
Con return_secret, el otro sistema añade sig al enlace de retorno: el HMAC-SHA256 en hexadecimal, con el secreto como clave, del ID de ejecución seguido de &<param>=<value> para cada parámetro de return_params, ordenados por nombre. Cada nombre y cada valor se codifica con porcentajes escapando todos los caracteres reservados, de modo que un valor que contenga & o = no puede hacerse pasar por otro parámetro. 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 retorno sin sig, o con uno que no coincide, recibe el estado 409 y la página "This return could not be confirmed". La página no se marca como terminada, así que un retorno firmado correctamente sigue funcionando después. El secreto tiene que quedarse en el servidor del otro sistema. Una encuesta alojada que construye su redirección final en el navegador del participante no puede mantenerlo privado, así que en ese caso la firma no sirve de nada.
- oTree: envía a los participantes a la URL de una sala con
participant_label={participant}. La aplicación tiene que llevar ella misma la URL de retorno: lee el parámetropotato_returncuando llega el participante, guárdalo enparticipant.varsy redirige a él desde la última página. Cómo lee la primera página los parámetros de la URL depende de la versión de oTree, así que consulta su documentación. - Empirica: pasa
{participant}como parámetro de URLparticipantKey. - Una encuesta o un experimento alojados (Qualtrics, Gorilla, Pavlovia): pon
{participant}en un parámetro de datos incrustados o de URL, y fija la redirección final en la URL de retorno.
Datos y exportación
python -m potato.export --config config.yaml --format runtime --output ./export/La exportación escribe estos archivos para cada tipo de ejecución que usa el proyecto, donde <kind> es jspsych, headphone_check o external:
| Archivo | Contenido |
|---|---|
<kind>_runs.csv | Una fila por ejecución: participante, página, intento, estado, número de ensayos y condiciones |
<kind>_trials.csv | Una fila por ensayo, con todas las columnas que registró la tarea. Los valores anidados van en JSON |
<kind>_interaction.csv | Eventos de pérdida de foco, foco y pantalla completa, cuando se registró alguno |
<kind>_raw/<user>/<page>-<run>.json | Los datos de cada ejecución tal como los produjo la tarea |
Las ejecuciones que no terminaron tienen el estado started o partial. Un ensayo de la comprobación de auriculares registra test, trial, index (su posición en la ejecución) y response (el sonido elegido, de 1 a 3), más tres valores que añade el servidor: target (el sonido correcto), correct y scored, que es false para una respuesta que el servidor no contó.
Seguridad y temporización
La línea de tiempo es código que has escrito tú, y se ejecuta en el origen del servidor de Potato con la sesión del participante, con la misma confianza que una página de instrucciones en HTML personalizada. Su Content-Security-Policy permite scripts, estilos y medios solo desde el servidor de Potato y desde cualquier csp.extra_sources, y peticiones de red solo hacia el servidor de Potato, así que un estímulo que muestra texto de un participante no puede cargar desde nada más ni enviar nada a otro sitio. Aun así, escapa ese texto antes de ponerlo en HTML.
Todo archivo que nombre la página debe estar dentro del directorio de la tarea. La ruta de recursos solo sirve tipos de archivo de estímulos, nunca .js ni .html de assets_dir, y envía cada uno con su propia CSP de aislamiento, así que un SVG abierto directamente no ejecuta ningún script. csp.extra_sources admite orígenes https:// individuales y rechaza los comodines.
En la página rige la temporización propia de jsPsych, porque los scripts de anotación de Potato no se cargan ahí. La documentación sobre la precisión de la temporización de jsPsych indica que los tiempos de respuesta medidos en JavaScript suelen ser entre 10 y 40 ms más largos que en el software de laboratorio, con una varianza similar.
Ejemplos
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 8000El primero es una tarea de Stroop entre un cuestionario de selección y una tarea de valoración de palabras, con retroalimentación mostrada a la mitad de los participantes. El segundo es una comprobación de auriculares seguida de fragmentos de habla valorados en la escala ACR.
Lecturas adicionales
- Flujo del experimento - Condiciones,
show_when,exit_wheny contrabalanceo - Si vienes de jsPsych - Conceptos de jsPsych asociados a claves de configuración de Potato
- Pruebas de escucha - MUSHRA, ACR y los demás métodos de pruebas de escucha
- Ensayos cronometrados - Respuestas rápidas en ítems de anotación sin una línea de tiempo de jsPsych
Para detalles de implementación, consulta la documentación fuente sobre las fases de ejecución y la fase de jsPsych.