bead 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""JATOS exporter for jsPsych experiments.
|
|
2
|
+
|
|
3
|
+
This module provides the JATOSExporter class for creating JATOS study packages (.jzip)
|
|
4
|
+
from generated jsPsych experiments.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import tempfile
|
|
13
|
+
import uuid
|
|
14
|
+
import zipfile
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from bead.data.base import BeadBaseModel, JsonValue
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class JATOSExporter(BeadBaseModel):
|
|
22
|
+
"""Exports jsPsych experiments as JATOS study packages (.jzip).
|
|
23
|
+
|
|
24
|
+
A .jzip file is a ZIP archive containing:
|
|
25
|
+
- study.json: JATOS metadata
|
|
26
|
+
- experiment/: All experiment files (HTML, JS, CSS, data)
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
study_title : str
|
|
31
|
+
Title of the JATOS study.
|
|
32
|
+
study_description : str
|
|
33
|
+
Description of the study.
|
|
34
|
+
|
|
35
|
+
Examples
|
|
36
|
+
--------
|
|
37
|
+
>>> from pathlib import Path
|
|
38
|
+
>>> exporter = JATOSExporter("Test Study", "A test study")
|
|
39
|
+
>>> # exporter.export(Path("experiment"), Path("study.jzip"))
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
study_title: str
|
|
43
|
+
study_description: str = ""
|
|
44
|
+
|
|
45
|
+
def export(
|
|
46
|
+
self,
|
|
47
|
+
experiment_dir: Path,
|
|
48
|
+
output_path: Path,
|
|
49
|
+
component_title: str = "Main Experiment",
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Create JATOS .jzip file.
|
|
52
|
+
|
|
53
|
+
Parameters
|
|
54
|
+
----------
|
|
55
|
+
experiment_dir : Path
|
|
56
|
+
Directory containing experiment files (from JsPsychExperimentGenerator).
|
|
57
|
+
Expected structure:
|
|
58
|
+
- index.html
|
|
59
|
+
- css/experiment.css
|
|
60
|
+
- js/experiment.js
|
|
61
|
+
- data/timeline.json
|
|
62
|
+
- data/config.json
|
|
63
|
+
output_path : Path
|
|
64
|
+
Output path for .jzip file.
|
|
65
|
+
component_title : str
|
|
66
|
+
Title for the JATOS component.
|
|
67
|
+
|
|
68
|
+
Raises
|
|
69
|
+
------
|
|
70
|
+
ValueError
|
|
71
|
+
If experiment_dir does not exist or is missing required files.
|
|
72
|
+
FileNotFoundError
|
|
73
|
+
If required experiment files are not found.
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> exporter = JATOSExporter("Test Study")
|
|
78
|
+
>>> exporter.export(Path("exp"), Path("study.jzip"))
|
|
79
|
+
"""
|
|
80
|
+
if not experiment_dir.exists():
|
|
81
|
+
raise ValueError(f"Experiment directory does not exist: {experiment_dir}")
|
|
82
|
+
|
|
83
|
+
if not experiment_dir.is_dir():
|
|
84
|
+
raise ValueError(f"Path is not a directory: {experiment_dir}")
|
|
85
|
+
|
|
86
|
+
# verify required files exist
|
|
87
|
+
required_files = ["index.html"]
|
|
88
|
+
for file in required_files:
|
|
89
|
+
if not (experiment_dir / file).exists():
|
|
90
|
+
raise FileNotFoundError(
|
|
91
|
+
f"Required file not found: {experiment_dir / file}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# create temporary directory for staging
|
|
95
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
96
|
+
temp_path = Path(temp_dir)
|
|
97
|
+
|
|
98
|
+
# create study.json
|
|
99
|
+
study_json = self._create_study_json(component_title)
|
|
100
|
+
study_json_path = temp_path / "study.json"
|
|
101
|
+
study_json_path.write_text(json.dumps(study_json, indent=2))
|
|
102
|
+
|
|
103
|
+
# copy experiment files to temp/experiment/
|
|
104
|
+
experiment_target = temp_path / "experiment"
|
|
105
|
+
shutil.copytree(experiment_dir, experiment_target)
|
|
106
|
+
|
|
107
|
+
# create .jzip file
|
|
108
|
+
with zipfile.ZipFile(output_path, "w", zipfile.ZIP_DEFLATED) as zipf:
|
|
109
|
+
# add study.json
|
|
110
|
+
zipf.write(study_json_path, "study.json")
|
|
111
|
+
|
|
112
|
+
# add all experiment files
|
|
113
|
+
for file_path in experiment_target.rglob("*"):
|
|
114
|
+
if file_path.is_file():
|
|
115
|
+
# archive name relative to temp_path
|
|
116
|
+
arcname = file_path.relative_to(temp_path)
|
|
117
|
+
zipf.write(file_path, arcname)
|
|
118
|
+
|
|
119
|
+
def _create_study_json(self, component_title: str) -> dict[str, JsonValue]:
|
|
120
|
+
"""Create JATOS study.json structure.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
component_title : str
|
|
125
|
+
Title for the JATOS component.
|
|
126
|
+
|
|
127
|
+
Returns
|
|
128
|
+
-------
|
|
129
|
+
dict[str, JsonValue]
|
|
130
|
+
JATOS study metadata dictionary.
|
|
131
|
+
|
|
132
|
+
Notes
|
|
133
|
+
-----
|
|
134
|
+
The study.json follows JATOS v3 schema format.
|
|
135
|
+
"""
|
|
136
|
+
# generate UUIDs for study and component
|
|
137
|
+
study_uuid = str(uuid.uuid4())
|
|
138
|
+
component_uuid = str(uuid.uuid4())
|
|
139
|
+
|
|
140
|
+
# sanitize title for directory name
|
|
141
|
+
dir_name = self._sanitize_dirname(self.study_title)
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
"version": "3",
|
|
145
|
+
"data": {
|
|
146
|
+
"uuid": study_uuid,
|
|
147
|
+
"title": self.study_title,
|
|
148
|
+
"description": self.study_description,
|
|
149
|
+
"dirName": dir_name,
|
|
150
|
+
"comments": f"Generated by bead {datetime.now().isoformat()}",
|
|
151
|
+
"jsonData": None,
|
|
152
|
+
"componentList": [
|
|
153
|
+
{
|
|
154
|
+
"uuid": component_uuid,
|
|
155
|
+
"title": component_title,
|
|
156
|
+
"htmlFilePath": "experiment/index.html",
|
|
157
|
+
"reloadable": True,
|
|
158
|
+
"active": True,
|
|
159
|
+
"comments": "",
|
|
160
|
+
"jsonData": None,
|
|
161
|
+
}
|
|
162
|
+
],
|
|
163
|
+
"batchList": [],
|
|
164
|
+
"groupStudy": False,
|
|
165
|
+
"linearStudy": False,
|
|
166
|
+
"allowPreview": True,
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
def _sanitize_dirname(self, title: str) -> str:
|
|
171
|
+
"""Sanitize study title for use as directory name.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
title : str
|
|
176
|
+
Study title to sanitize.
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
str
|
|
181
|
+
Sanitized directory name.
|
|
182
|
+
|
|
183
|
+
Examples
|
|
184
|
+
--------
|
|
185
|
+
>>> exporter = JATOSExporter("My Study")
|
|
186
|
+
>>> exporter._sanitize_dirname("My Study")
|
|
187
|
+
'my_study'
|
|
188
|
+
>>> exporter._sanitize_dirname("Study (2024)")
|
|
189
|
+
'study_2024'
|
|
190
|
+
"""
|
|
191
|
+
# convert to lowercase
|
|
192
|
+
dirname = title.lower()
|
|
193
|
+
|
|
194
|
+
# replace spaces with underscores
|
|
195
|
+
dirname = dirname.replace(" ", "_")
|
|
196
|
+
|
|
197
|
+
# remove non-alphanumeric characters (except underscores)
|
|
198
|
+
dirname = re.sub(r"[^a-z0-9_]", "", dirname)
|
|
199
|
+
|
|
200
|
+
# remove consecutive underscores
|
|
201
|
+
dirname = re.sub(r"_+", "_", dirname)
|
|
202
|
+
|
|
203
|
+
# remove leading/trailing underscores
|
|
204
|
+
dirname = dirname.strip("_")
|
|
205
|
+
|
|
206
|
+
# ensure it's not empty
|
|
207
|
+
if not dirname:
|
|
208
|
+
dirname = "study"
|
|
209
|
+
|
|
210
|
+
return dirname
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""jsPsych 8.x deployment components.
|
|
2
|
+
|
|
3
|
+
Generates jsPsych experiments with batch mode support and server-side list
|
|
4
|
+
distribution via JATOS batch sessions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from bead.deployment.jspsych.randomizer import generate_randomizer_function
|
|
8
|
+
|
|
9
|
+
__all__ = ["generate_randomizer_function"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"include": ["src/**/*.ts"],
|
|
10
|
+
"ignore": ["dist", "node_modules"]
|
|
11
|
+
},
|
|
12
|
+
"organizeImports": {
|
|
13
|
+
"enabled": true
|
|
14
|
+
},
|
|
15
|
+
"linter": {
|
|
16
|
+
"enabled": true,
|
|
17
|
+
"rules": {
|
|
18
|
+
"recommended": true,
|
|
19
|
+
"style": {
|
|
20
|
+
"noNonNullAssertion": "off",
|
|
21
|
+
"useNodejsImportProtocol": "off"
|
|
22
|
+
},
|
|
23
|
+
"suspicious": {
|
|
24
|
+
"noExplicitAny": "warn"
|
|
25
|
+
},
|
|
26
|
+
"complexity": {
|
|
27
|
+
"useLiteralKeys": "off"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"formatter": {
|
|
32
|
+
"enabled": true,
|
|
33
|
+
"indentStyle": "space",
|
|
34
|
+
"indentWidth": 2,
|
|
35
|
+
"lineWidth": 100
|
|
36
|
+
},
|
|
37
|
+
"javascript": {
|
|
38
|
+
"formatter": {
|
|
39
|
+
"quoteStyle": "double",
|
|
40
|
+
"trailingCommas": "all",
|
|
41
|
+
"semicolons": "always"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Configuration models for jsPsych experiment generation.
|
|
2
|
+
|
|
3
|
+
This module provides Pydantic models for configuring jsPsych experiment
|
|
4
|
+
generation, including experiment types, UI settings, and display options.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
|
+
|
|
13
|
+
from bead.config.deployment import SlopitIntegrationConfig
|
|
14
|
+
from bead.data.range import Range
|
|
15
|
+
from bead.deployment.distribution import ListDistributionStrategy
|
|
16
|
+
|
|
17
|
+
# Type alias for experiment types
|
|
18
|
+
type ExperimentType = Literal[
|
|
19
|
+
"likert_rating",
|
|
20
|
+
"slider_rating",
|
|
21
|
+
"binary_choice",
|
|
22
|
+
"forced_choice",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Type alias for UI themes
|
|
26
|
+
type UITheme = Literal["light", "dark", "auto"]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Factory functions for default lists
|
|
30
|
+
def _empty_demographics_fields() -> list[DemographicsFieldConfig]:
|
|
31
|
+
"""Return empty demographics field list."""
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _empty_instruction_pages() -> list[InstructionPage]:
|
|
36
|
+
"""Return empty instruction pages list."""
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class DemographicsFieldConfig(BaseModel):
|
|
41
|
+
"""Configuration for a single demographics form field.
|
|
42
|
+
|
|
43
|
+
Used to configure fields in a demographics form that appears before
|
|
44
|
+
the experiment instructions. Supports various input types including
|
|
45
|
+
text, number, dropdown, radio buttons, and checkboxes.
|
|
46
|
+
|
|
47
|
+
Attributes
|
|
48
|
+
----------
|
|
49
|
+
name : str
|
|
50
|
+
Field name (used as key in collected data).
|
|
51
|
+
field_type : Literal["text", "number", "dropdown", "radio", "checkbox"]
|
|
52
|
+
Type of form input.
|
|
53
|
+
label : str
|
|
54
|
+
Display label for the field.
|
|
55
|
+
required : bool
|
|
56
|
+
Whether this field is required (default: False).
|
|
57
|
+
options : list[str] | None
|
|
58
|
+
Options for dropdown/radio fields (default: None).
|
|
59
|
+
range : Range[int] | Range[float] | None
|
|
60
|
+
Numeric range constraint for number fields (default: None).
|
|
61
|
+
placeholder : str | None
|
|
62
|
+
Placeholder text for text/number inputs (default: None).
|
|
63
|
+
help_text : str | None
|
|
64
|
+
Help text displayed below the field (default: None).
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> age_field = DemographicsFieldConfig(
|
|
69
|
+
... name="age",
|
|
70
|
+
... field_type="number",
|
|
71
|
+
... label="Your Age",
|
|
72
|
+
... required=True,
|
|
73
|
+
... range=Range[int](min=18, max=100),
|
|
74
|
+
... )
|
|
75
|
+
>>> education_field = DemographicsFieldConfig(
|
|
76
|
+
... name="education",
|
|
77
|
+
... field_type="dropdown",
|
|
78
|
+
... label="Highest Education Level",
|
|
79
|
+
... required=True,
|
|
80
|
+
... options=["High School", "Bachelor's", "Master's", "PhD"],
|
|
81
|
+
... )
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
85
|
+
|
|
86
|
+
name: str
|
|
87
|
+
field_type: Literal["text", "number", "dropdown", "radio", "checkbox"]
|
|
88
|
+
label: str
|
|
89
|
+
required: bool = False
|
|
90
|
+
options: list[str] | None = None
|
|
91
|
+
range: Range[int] | Range[float] | None = None
|
|
92
|
+
placeholder: str | None = None
|
|
93
|
+
help_text: str | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DemographicsConfig(BaseModel):
|
|
97
|
+
"""Configuration for participant demographics form.
|
|
98
|
+
|
|
99
|
+
Defines a demographics form that appears before experiment instructions.
|
|
100
|
+
When enabled, participants must complete this form before proceeding.
|
|
101
|
+
|
|
102
|
+
Attributes
|
|
103
|
+
----------
|
|
104
|
+
enabled : bool
|
|
105
|
+
Whether to show the demographics form (default: False).
|
|
106
|
+
title : str
|
|
107
|
+
Title displayed at the top of the form (default: "Participant Information").
|
|
108
|
+
fields : list[DemographicsFieldConfig]
|
|
109
|
+
List of fields to include in the form.
|
|
110
|
+
submit_button_text : str
|
|
111
|
+
Text for the submit button (default: "Continue").
|
|
112
|
+
|
|
113
|
+
Examples
|
|
114
|
+
--------
|
|
115
|
+
>>> config = DemographicsConfig(
|
|
116
|
+
... enabled=True,
|
|
117
|
+
... title="About You",
|
|
118
|
+
... fields=[
|
|
119
|
+
... DemographicsFieldConfig(
|
|
120
|
+
... name="age",
|
|
121
|
+
... field_type="number",
|
|
122
|
+
... label="Age",
|
|
123
|
+
... required=True,
|
|
124
|
+
... ),
|
|
125
|
+
... ],
|
|
126
|
+
... )
|
|
127
|
+
>>> config.enabled
|
|
128
|
+
True
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
model_config = ConfigDict(extra="forbid")
|
|
132
|
+
|
|
133
|
+
enabled: bool = False
|
|
134
|
+
title: str = "Participant Information"
|
|
135
|
+
fields: list[DemographicsFieldConfig] = Field(
|
|
136
|
+
default_factory=_empty_demographics_fields
|
|
137
|
+
)
|
|
138
|
+
submit_button_text: str = "Continue"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class InstructionPage(BaseModel):
|
|
142
|
+
"""A single instruction page for multi-page instructions.
|
|
143
|
+
|
|
144
|
+
Attributes
|
|
145
|
+
----------
|
|
146
|
+
content : str
|
|
147
|
+
HTML content for this page.
|
|
148
|
+
title : str | None
|
|
149
|
+
Optional title for this page (displayed above content).
|
|
150
|
+
|
|
151
|
+
Examples
|
|
152
|
+
--------
|
|
153
|
+
>>> page = InstructionPage(
|
|
154
|
+
... title="Welcome",
|
|
155
|
+
... content="<p>Thank you for participating in this study.</p>",
|
|
156
|
+
... )
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
160
|
+
|
|
161
|
+
content: str
|
|
162
|
+
title: str | None = None
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class InstructionsConfig(BaseModel):
|
|
166
|
+
"""Configuration for multi-page experiment instructions.
|
|
167
|
+
|
|
168
|
+
Allows creating rich, multi-page instructions with navigation controls.
|
|
169
|
+
Participants can optionally navigate backwards through pages.
|
|
170
|
+
|
|
171
|
+
Attributes
|
|
172
|
+
----------
|
|
173
|
+
pages : list[InstructionPage]
|
|
174
|
+
List of instruction pages to display.
|
|
175
|
+
show_page_numbers : bool
|
|
176
|
+
Whether to show page numbers (default: True).
|
|
177
|
+
allow_backwards : bool
|
|
178
|
+
Whether to allow navigating to previous pages (default: True).
|
|
179
|
+
button_label_next : str
|
|
180
|
+
Label for the next button (default: "Next").
|
|
181
|
+
button_label_finish : str
|
|
182
|
+
Label for the final button (default: "Begin Experiment").
|
|
183
|
+
|
|
184
|
+
Examples
|
|
185
|
+
--------
|
|
186
|
+
>>> config = InstructionsConfig(
|
|
187
|
+
... pages=[
|
|
188
|
+
... InstructionPage(title="Welcome", content="<p>Welcome!</p>"),
|
|
189
|
+
... InstructionPage(title="Task", content="<p>Your task is...</p>"),
|
|
190
|
+
... ],
|
|
191
|
+
... allow_backwards=True,
|
|
192
|
+
... )
|
|
193
|
+
>>> len(config.pages)
|
|
194
|
+
2
|
|
195
|
+
|
|
196
|
+
>>> # Create from plain text (single page)
|
|
197
|
+
>>> config = InstructionsConfig.from_text("Please rate each sentence.")
|
|
198
|
+
>>> len(config.pages)
|
|
199
|
+
1
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
model_config = ConfigDict(extra="forbid")
|
|
203
|
+
|
|
204
|
+
pages: list[InstructionPage] = Field(default_factory=_empty_instruction_pages)
|
|
205
|
+
show_page_numbers: bool = True
|
|
206
|
+
allow_backwards: bool = True
|
|
207
|
+
button_label_next: str = "Next"
|
|
208
|
+
button_label_finish: str = "Begin Experiment"
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_text(cls, text: str) -> InstructionsConfig:
|
|
212
|
+
"""Create single-page instructions from plain text.
|
|
213
|
+
|
|
214
|
+
Provides backward compatibility with simple string instructions.
|
|
215
|
+
|
|
216
|
+
Parameters
|
|
217
|
+
----------
|
|
218
|
+
text : str
|
|
219
|
+
Plain text or HTML content for a single instruction page.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
InstructionsConfig
|
|
224
|
+
Instructions config with a single page.
|
|
225
|
+
|
|
226
|
+
Examples
|
|
227
|
+
--------
|
|
228
|
+
>>> config = InstructionsConfig.from_text("Rate each item from 1-7.")
|
|
229
|
+
>>> config.pages[0].content
|
|
230
|
+
'Rate each item from 1-7.'
|
|
231
|
+
"""
|
|
232
|
+
return cls(pages=[InstructionPage(content=text)])
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class ExperimentConfig(BaseModel):
|
|
236
|
+
"""Configuration for jsPsych experiment generation.
|
|
237
|
+
|
|
238
|
+
Defines all configurable aspects of a jsPsych experiment, including experiment
|
|
239
|
+
type, UI settings, trial presentation options, and list distribution strategy.
|
|
240
|
+
|
|
241
|
+
Attributes
|
|
242
|
+
----------
|
|
243
|
+
experiment_type : ExperimentType
|
|
244
|
+
Type of experiment (likert_rating, slider_rating, binary_choice, forced_choice)
|
|
245
|
+
title : str
|
|
246
|
+
Experiment title displayed to participants
|
|
247
|
+
description : str
|
|
248
|
+
Brief description of the experiment
|
|
249
|
+
instructions : str | InstructionsConfig
|
|
250
|
+
Instructions shown to participants before the experiment. Can be a simple
|
|
251
|
+
string (single page) or InstructionsConfig for multi-page instructions.
|
|
252
|
+
demographics : DemographicsConfig | None
|
|
253
|
+
Optional demographics form shown before instructions (default: None).
|
|
254
|
+
When provided and enabled, participants must complete this form first.
|
|
255
|
+
distribution_strategy : ListDistributionStrategy
|
|
256
|
+
List distribution strategy for batch mode (required, no default).
|
|
257
|
+
Specifies how participants are assigned to experiment lists using JATOS
|
|
258
|
+
batch sessions. See bead.deployment.distribution for available strategies.
|
|
259
|
+
randomize_trial_order : bool
|
|
260
|
+
Whether to randomize trial order (default: True)
|
|
261
|
+
show_progress_bar : bool
|
|
262
|
+
Whether to show a progress bar during the experiment (default: True)
|
|
263
|
+
ui_theme : UITheme
|
|
264
|
+
UI theme for the experiment (light, dark, auto; default: light)
|
|
265
|
+
on_finish_url : str | None
|
|
266
|
+
URL to redirect to after experiment completion (default: None)
|
|
267
|
+
If prolific_completion_code is set, this will be auto-generated
|
|
268
|
+
allow_backwards : bool
|
|
269
|
+
Whether participants can go back to previous trials (default: False)
|
|
270
|
+
show_click_target : bool
|
|
271
|
+
Whether to show click target for accuracy tracking (default: False)
|
|
272
|
+
minimum_duration_ms : int
|
|
273
|
+
Minimum trial duration in milliseconds (default: 0)
|
|
274
|
+
use_jatos : bool
|
|
275
|
+
Whether to enable JATOS integration (default: True)
|
|
276
|
+
prolific_completion_code : str | None
|
|
277
|
+
Prolific completion code for automatic redirect URL generation (default: None)
|
|
278
|
+
When set, on_finish_url will be auto-generated as:
|
|
279
|
+
https://app.prolific.co/submissions/complete?cc=<code>
|
|
280
|
+
slopit : SlopitIntegrationConfig
|
|
281
|
+
Slopit behavioral capture integration configuration (default: disabled).
|
|
282
|
+
When enabled, captures keystroke dynamics, focus patterns, and paste events
|
|
283
|
+
during experiment trials for AI-assisted response detection.
|
|
284
|
+
|
|
285
|
+
Examples
|
|
286
|
+
--------
|
|
287
|
+
>>> from bead.deployment.distribution import (
|
|
288
|
+
... ListDistributionStrategy,
|
|
289
|
+
... DistributionStrategyType
|
|
290
|
+
... )
|
|
291
|
+
>>> strategy = ListDistributionStrategy(
|
|
292
|
+
... strategy_type=DistributionStrategyType.BALANCED,
|
|
293
|
+
... max_participants=100
|
|
294
|
+
... )
|
|
295
|
+
>>> config = ExperimentConfig(
|
|
296
|
+
... experiment_type="likert_rating",
|
|
297
|
+
... title="Sentence Acceptability Study",
|
|
298
|
+
... description="Rate the acceptability of sentences",
|
|
299
|
+
... instructions="Please rate each sentence on a scale from 1 to 7.",
|
|
300
|
+
... distribution_strategy=strategy
|
|
301
|
+
... )
|
|
302
|
+
>>> config.randomize_trial_order
|
|
303
|
+
True
|
|
304
|
+
>>> config.ui_theme
|
|
305
|
+
'light'
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
model_config = ConfigDict(
|
|
309
|
+
extra="forbid",
|
|
310
|
+
frozen=False,
|
|
311
|
+
validate_assignment=True,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
experiment_type: ExperimentType
|
|
315
|
+
title: str
|
|
316
|
+
description: str
|
|
317
|
+
instructions: str | InstructionsConfig
|
|
318
|
+
distribution_strategy: ListDistributionStrategy
|
|
319
|
+
demographics: DemographicsConfig | None = Field(
|
|
320
|
+
default=None,
|
|
321
|
+
description="Demographics form shown before instructions",
|
|
322
|
+
)
|
|
323
|
+
randomize_trial_order: bool = Field(default=True)
|
|
324
|
+
show_progress_bar: bool = Field(default=True)
|
|
325
|
+
ui_theme: UITheme = Field(default="light")
|
|
326
|
+
on_finish_url: str | None = Field(default=None)
|
|
327
|
+
allow_backwards: bool = Field(default=False)
|
|
328
|
+
show_click_target: bool = Field(default=False)
|
|
329
|
+
minimum_duration_ms: int = Field(default=0, ge=0)
|
|
330
|
+
use_jatos: bool = Field(default=True)
|
|
331
|
+
prolific_completion_code: str | None = Field(default=None)
|
|
332
|
+
slopit: SlopitIntegrationConfig = Field(
|
|
333
|
+
default_factory=SlopitIntegrationConfig,
|
|
334
|
+
description="Slopit behavioral capture integration (opt-in, disabled)",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class RatingScaleConfig(BaseModel):
|
|
339
|
+
"""Configuration for rating scale trials.
|
|
340
|
+
|
|
341
|
+
Attributes
|
|
342
|
+
----------
|
|
343
|
+
scale
|
|
344
|
+
Numeric range for the rating scale with min and max values.
|
|
345
|
+
Default is Range(min=1, max=7) for a standard 7-point Likert scale.
|
|
346
|
+
min_label
|
|
347
|
+
Label for the minimum value (default: "Not at all").
|
|
348
|
+
max_label
|
|
349
|
+
Label for the maximum value (default: "Very much").
|
|
350
|
+
step
|
|
351
|
+
Step size between values (default: 1).
|
|
352
|
+
show_numeric_labels
|
|
353
|
+
Whether to show numeric labels on the scale (default: True).
|
|
354
|
+
required
|
|
355
|
+
Whether a response is required (default: True).
|
|
356
|
+
|
|
357
|
+
Examples
|
|
358
|
+
--------
|
|
359
|
+
>>> config = RatingScaleConfig()
|
|
360
|
+
>>> config.scale.min
|
|
361
|
+
1
|
|
362
|
+
>>> config.scale.max
|
|
363
|
+
7
|
|
364
|
+
>>> config.scale.contains(4)
|
|
365
|
+
True
|
|
366
|
+
|
|
367
|
+
>>> # Custom 5-point scale
|
|
368
|
+
>>> config = RatingScaleConfig(scale=Range[int](min=1, max=5))
|
|
369
|
+
>>> config.scale.max
|
|
370
|
+
5
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
model_config = ConfigDict(
|
|
374
|
+
extra="forbid",
|
|
375
|
+
frozen=False,
|
|
376
|
+
validate_assignment=True,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
scale: Range[int] = Field(
|
|
380
|
+
default_factory=lambda: Range[int](min=1, max=7),
|
|
381
|
+
description="Numeric range for the rating scale",
|
|
382
|
+
)
|
|
383
|
+
min_label: str = Field(default="Not at all")
|
|
384
|
+
max_label: str = Field(default="Very much")
|
|
385
|
+
step: int = Field(default=1, ge=1)
|
|
386
|
+
show_numeric_labels: bool = Field(default=True)
|
|
387
|
+
required: bool = Field(default=True)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class ChoiceConfig(BaseModel):
|
|
391
|
+
"""Configuration for choice trials.
|
|
392
|
+
|
|
393
|
+
Attributes
|
|
394
|
+
----------
|
|
395
|
+
button_html : str | None
|
|
396
|
+
Custom HTML for choice buttons (default: None)
|
|
397
|
+
required : bool
|
|
398
|
+
Whether a response is required (default: True)
|
|
399
|
+
randomize_choice_order : bool
|
|
400
|
+
Whether to randomize the order of choices (default: False)
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
model_config = ConfigDict(
|
|
404
|
+
extra="forbid",
|
|
405
|
+
frozen=False,
|
|
406
|
+
validate_assignment=True,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
button_html: str | None = Field(default=None)
|
|
410
|
+
required: bool = Field(default=True)
|
|
411
|
+
randomize_choice_order: bool = Field(default=False)
|