Spaces:
Running
Running
Deploy 6b6536c from GitHub
Browse files
README.md
CHANGED
|
@@ -26,11 +26,13 @@ participant ──► Space (public URL, no login)
|
|
| 26 |
→ <RESPONSES_REPO>/responses/listNN/<session>.json
|
| 27 |
```
|
| 28 |
|
| 29 |
-
**
|
|
|
|
|
|
|
| 30 |
|
| 31 |
**Session.** Consent, a two-question profile, instructions, one practice image ("A butterfly on a flower", `static/practice/`, not saved; a practice ranking with three images also opens the ranking part), then the list in random order. Each image gets one question, the 1–5 match rating. **Question 2 is turned off for now** (commented out in `static/index.html`, marked "question 2"; the segmentation maps are then not loaded either); its fields stay in the saved trials, with `problem` = null. When on, question 2 asks “Is there anything in the image that doesn’t match the text?”. Answering *Yes* requires selecting at least one part: on images with a segmentation in the dataset (`segmentation/<image_id>.png`, 101 images today), a click selects the whole segment under it (saved as `segments`, with each segment's `category` and `names`, plus one point per segment in `marks`; `marking` = `segments`); on the other images, or when a segmentation is missing, unreadable or empty, it places a point (`marking` = `points`). Category names are never sent to the browser. Question 2 is saved as `problem` (sessions before this change saved the older "should not be there" question as `unwanted`). Leaving or reloading the page asks for confirmation, since it loses the answers. The page shows at once: the images of step 1 load while the participant reads the consent and instructions, those of step 2 on the ranking instructions, and a loading screen appears only if an image is not ready when its turn comes. For each image the session stores its `position` (presentation order) and `rt` (time on the image, from display to *Next*).
|
| 32 |
|
| 33 |
-
**Rankings.** After the ratings, each participant ranks `RANKINGS_PER_LIST` groups (default 5). The groups come from the dataset, `groups/groups.jsonl`: one text and its images (`members`, any number from 2; a group with a single image is skipped), so editing that file and restarting the Space changes the rankings. Images that are only in a group (no `sample_id`) are shown only there. The question is match first, preference second: participants order the images by how well each fits the text and, among images that fit equally well (e.g., where the text leaves a detail open), put first the one they would rather get. They tap the images from best to worst, the last one included, shown in random order. Groups are spread evenly over the lists, never two with the same text in one list (16 groups today, each in about 3
|
| 34 |
|
| 35 |
**Progress.** Every page shows the same two-step bar, *1 · Rate images* then *2 · Rank images*, with the image or ranking number under it. The page adapts to phones: images fit the screen height, rankings use 2 columns on a phone and images up to 400 px wide on a large screen (3 per row, 2 × 2 for 4 images).
|
| 36 |
|
|
@@ -131,6 +133,14 @@ python analysis/collect_responses.py Enzo-py/partial-prompting-responses --out r
|
|
| 131 |
```
|
| 132 |
Downloads every session, flattens to one row per trial, and adds one column per quality check plus `n_failed_checks` and `excluded`. Rankings go to `rankings.parquet`, one row per ranked image.
|
| 133 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
## Files
|
| 135 |
|
| 136 |
```
|
|
|
|
| 26 |
→ <RESPONSES_REPO>/responses/listNN/<session>.json
|
| 27 |
```
|
| 28 |
|
| 29 |
+
**Lists.** When every study row of the dataset has a `list` (today: 10 lists of 18 stimuli, 17 in the last, 2 of them attention checks, from the dataset's `tools/assign_lists.py`), the Space uses those lists as they are, and the dataset's `attention_check` flag marks the checks; `LIST_COUNT`, `LIST_SIZE` and `CHECKS_PER_LIST` are then unused. Those lists keep the members of a pair and the uses of an image apart but may give one participant two stimuli with the same text; the Space logs each such case at startup. Without a `list` column, the Space builds the lists itself as follows.
|
| 30 |
+
|
| 31 |
+
**Counterbalancing.** Only stimuli with `human_study` true are used. The attention checks are the controls whose image plainly contradicts the text (`role` = `control` with a `control_type`); other controls (such as the "every detail true" condition of F pairs) are study stimuli like the rest. Each list gets `CHECKS_PER_LIST` attention checks (default 2, rotating and balanced across lists) and up to `LIST_SIZE` study images (default 14, for a session of about 9 minutes). Two stimuli never go in the same list when they belong to the same minimal pair (`pair_ids`), reuse the same image (`image_id`) or share the same text, so every comparison is between-subjects and nobody sees a text twice. By default there are just enough lists to show every study stimulus at least once, and at least as many as the largest group of stimuli sharing a text, an image or a pair; `LIST_COUNT` forces a number. The search is deterministic: the same dataset always gives the same lists. They are written to `lists.json` (with how many lists each stimulus is in) and `/api/status` shows the sizes. With a dataset without lists, on today's stimuli: 10 lists of 16 images (2 checks + 14). Recruit participants in multiples of the number of lists.
|
| 32 |
|
| 33 |
**Session.** Consent, a two-question profile, instructions, one practice image ("A butterfly on a flower", `static/practice/`, not saved; a practice ranking with three images also opens the ranking part), then the list in random order. Each image gets one question, the 1–5 match rating. **Question 2 is turned off for now** (commented out in `static/index.html`, marked "question 2"; the segmentation maps are then not loaded either); its fields stay in the saved trials, with `problem` = null. When on, question 2 asks “Is there anything in the image that doesn’t match the text?”. Answering *Yes* requires selecting at least one part: on images with a segmentation in the dataset (`segmentation/<image_id>.png`, 101 images today), a click selects the whole segment under it (saved as `segments`, with each segment's `category` and `names`, plus one point per segment in `marks`; `marking` = `segments`); on the other images, or when a segmentation is missing, unreadable or empty, it places a point (`marking` = `points`). Category names are never sent to the browser. Question 2 is saved as `problem` (sessions before this change saved the older "should not be there" question as `unwanted`). Leaving or reloading the page asks for confirmation, since it loses the answers. The page shows at once: the images of step 1 load while the participant reads the consent and instructions, those of step 2 on the ranking instructions, and a loading screen appears only if an image is not ready when its turn comes. For each image the session stores its `position` (presentation order) and `rt` (time on the image, from display to *Next*).
|
| 34 |
|
| 35 |
+
**Rankings.** After the ratings, each participant ranks `RANKINGS_PER_LIST` groups (default 5). The groups come from the dataset, `groups/groups.jsonl`: one text and its images (`members`, any number from 2; a group with a single image is skipped), so editing that file and restarting the Space changes the rankings. Images that are only in a group (no `sample_id`) are shown only there. The question is match first, preference second: participants order the images by how well each fits the text and, among images that fit equally well (e.g., where the text leaves a detail open), put first the one they would rather get. They tap the images from best to worst, the last one included, shown in random order. Groups are spread evenly over the lists, never two with the same text in one list (16 groups today, each in about 3 lists). They come last so that seeing several images of one text cannot affect the between-subjects ratings. Each ranking saves `set_id` (the `group_id`), `shown` (screen order) and `order` (best first) as `image_id`s, `order_sample_ids` (the matching stimulus, null for group-only images), `rt` and `resets`.
|
| 36 |
|
| 37 |
**Progress.** Every page shows the same two-step bar, *1 · Rate images* then *2 · Rank images*, with the image or ranking number under it. The page adapts to phones: images fit the screen height, rankings use 2 columns on a phone and images up to 400 px wide on a large screen (3 per row, 2 × 2 for 4 images).
|
| 38 |
|
|
|
|
| 133 |
```
|
| 134 |
Downloads every session, flattens to one row per trial, and adds one column per quality check plus `n_failed_checks` and `excluded`. Rankings go to `rankings.parquet`, one row per ranked image.
|
| 135 |
|
| 136 |
+
To fill the dataset's post-study fields (`family_human` in `pairs/pairs.jsonl`, `ranking_human` in `groups/groups.jsonl`):
|
| 137 |
+
|
| 138 |
+
```bash
|
| 139 |
+
HF_TOKEN=... python analysis/export_human_results.py Enzo-py/partial-prompting-responses --out human_results
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
It writes both files in the dataset's format (every line and field kept, ready to replace the dataset's), plus `human/` with the numbers behind them: ratings per stimulus, the test-minus-reference difference with its 95% interval per pair, mean ranks per group, and each session's flags. Same exclusions as the Space. A pair is `sensitivity` when the interval lies below -0.5, `invariance` when it lies within ±0.5, else `open`; null under 10 ratings per member (`--margin`, `--min-ratings`, `--min-rankings`). A local folder of session files works in place of the repo id.
|
| 143 |
+
|
| 144 |
## Files
|
| 145 |
|
| 146 |
```
|
app.py
CHANGED
|
@@ -27,8 +27,9 @@ Environment variables (set them as Space secrets / variables):
|
|
| 27 |
STUDY_CONTACT optional; researcher contact shown on the consent page
|
| 28 |
PID_SALT optional secret; saves a salted hash of the Prolific ID (never the ID itself)
|
| 29 |
LIST_COUNT, LIST_SIZE optional; number of lists (default: just enough to show every stimulus) and
|
| 30 |
-
study images per list (default 14, at most)
|
| 31 |
-
|
|
|
|
| 32 |
RANKINGS_PER_LIST optional; ranking questions after the ratings (default 5, 0 = none)
|
| 33 |
"""
|
| 34 |
import hashlib
|
|
@@ -276,9 +277,11 @@ def in_study(s):
|
|
| 276 |
|
| 277 |
|
| 278 |
def is_attention_check(s):
|
| 279 |
-
"""
|
| 280 |
-
|
| 281 |
-
|
|
|
|
|
|
|
| 282 |
return s.get("role") == "control" and bool(
|
| 283 |
s.get("control_type") or s.get("subcategory") == "Attention check" or s.get("case_id") == "E1")
|
| 284 |
|
|
@@ -343,13 +346,34 @@ def design_lists(ids, neighbours, n_lists, size, fixed=None, seed=0, restarts=10
|
|
| 343 |
return best[1] if best else None
|
| 344 |
|
| 345 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 346 |
def build_lists(stimuli):
|
| 347 |
-
"""
|
| 348 |
-
|
|
|
|
| 349 |
Returns (lists, attention checks, stimuli not in the study).
|
| 350 |
"""
|
| 351 |
study = [s for s in stimuli if in_study(s)]
|
| 352 |
checks = sorted(s["sample_id"] for s in study if is_attention_check(s))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
items = sorted(s["sample_id"] for s in study if not is_attention_check(s))
|
| 354 |
neighbours = {s["sample_id"]: set() for s in study}
|
| 355 |
for a in study:
|
|
@@ -375,7 +399,6 @@ def build_lists(stimuli):
|
|
| 375 |
break
|
| 376 |
else:
|
| 377 |
check_lists, lists = [[]], [[]]
|
| 378 |
-
not_shown = [s["sample_id"] for s in stimuli if not in_study(s)]
|
| 379 |
return [sorted(c + l) for c, l in zip(check_lists, lists)], checks, not_shown
|
| 380 |
|
| 381 |
|
|
@@ -402,8 +425,8 @@ LIST_RANKINGS = assign_rankings(RANKING_SETS, len(LISTS))
|
|
| 402 |
{"n_lists": len(LISTS), "attention_checks": CHECKS, "not_in_study": NOT_IN_STUDY,
|
| 403 |
"exposure": dict(sorted(EXPOSURE.items())), "lists": LISTS,
|
| 404 |
"ranking_sets": RANKING_SETS, "list_rankings": LIST_RANKINGS}, indent=2))
|
| 405 |
-
print(f"{len(LISTS)} lists of {sorted({len(l) for l in LISTS})} images
|
| 406 |
-
f"checks each; {len(NOT_IN_STUDY)} stimuli not in the study (human_study = false); "
|
| 407 |
f"{len(RANKING_SETS)} ranking groups, {len(LIST_RANKINGS[0])} per list")
|
| 408 |
|
| 409 |
|
|
|
|
| 27 |
STUDY_CONTACT optional; researcher contact shown on the consent page
|
| 28 |
PID_SALT optional secret; saves a salted hash of the Prolific ID (never the ID itself)
|
| 29 |
LIST_COUNT, LIST_SIZE optional; number of lists (default: just enough to show every stimulus) and
|
| 30 |
+
study images per list (default 14, at most); unused when the dataset has its
|
| 31 |
+
own lists (`list` on every row)
|
| 32 |
+
CHECKS_PER_LIST optional; attention checks per list, rotating (default 2); same
|
| 33 |
RANKINGS_PER_LIST optional; ranking questions after the ratings (default 5, 0 = none)
|
| 34 |
"""
|
| 35 |
import hashlib
|
|
|
|
| 277 |
|
| 278 |
|
| 279 |
def is_attention_check(s):
|
| 280 |
+
"""The dataset's `attention_check` flag when it has one. Older datasets (and sessions saved with
|
| 281 |
+
them): a control whose text the image plainly contradicts (`control_type`: absent, relation, ...),
|
| 282 |
+
or case E1; other controls are the control condition of a pair, counterbalanced like the rest."""
|
| 283 |
+
if "attention_check" in s:
|
| 284 |
+
return bool(s["attention_check"])
|
| 285 |
return s.get("role") == "control" and bool(
|
| 286 |
s.get("control_type") or s.get("subcategory") == "Attention check" or s.get("case_id") == "E1")
|
| 287 |
|
|
|
|
| 346 |
return best[1] if best else None
|
| 347 |
|
| 348 |
|
| 349 |
+
def dataset_lists(study):
|
| 350 |
+
"""The lists the dataset defines (`list` on every study row, from its tools/assign_lists.py), or
|
| 351 |
+
None when it does not. Stimuli that conflict (see `conflict`) in one list are only reported."""
|
| 352 |
+
if not study or any(not isinstance(s.get("list"), int) for s in study):
|
| 353 |
+
return None
|
| 354 |
+
lists = defaultdict(list)
|
| 355 |
+
for s in study:
|
| 356 |
+
lists[s["list"]].append(s)
|
| 357 |
+
for k, l in sorted(lists.items()):
|
| 358 |
+
for i, a in enumerate(l):
|
| 359 |
+
for b in l[i + 1:]:
|
| 360 |
+
if conflict(a, b):
|
| 361 |
+
print(f"Note: list {k} has {a['sample_id']} and {b['sample_id']} together (same text, image or pair)")
|
| 362 |
+
return [sorted(s["sample_id"] for s in lists[k]) for k in sorted(lists)]
|
| 363 |
+
|
| 364 |
+
|
| 365 |
def build_lists(stimuli):
|
| 366 |
+
"""The dataset's own lists when every study row has a `list`. Otherwise counterbalanced lists built
|
| 367 |
+
here: CHECKS_PER_LIST attention checks plus up to LIST_SIZE study images each, never two
|
| 368 |
+
conflicting stimuli together (see `conflict`), so each comparison is between-subjects.
|
| 369 |
Returns (lists, attention checks, stimuli not in the study).
|
| 370 |
"""
|
| 371 |
study = [s for s in stimuli if in_study(s)]
|
| 372 |
checks = sorted(s["sample_id"] for s in study if is_attention_check(s))
|
| 373 |
+
not_shown = [s["sample_id"] for s in stimuli if not in_study(s)]
|
| 374 |
+
given = dataset_lists(study)
|
| 375 |
+
if given:
|
| 376 |
+
return given, checks, not_shown
|
| 377 |
items = sorted(s["sample_id"] for s in study if not is_attention_check(s))
|
| 378 |
neighbours = {s["sample_id"]: set() for s in study}
|
| 379 |
for a in study:
|
|
|
|
| 399 |
break
|
| 400 |
else:
|
| 401 |
check_lists, lists = [[]], [[]]
|
|
|
|
| 402 |
return [sorted(c + l) for c, l in zip(check_lists, lists)], checks, not_shown
|
| 403 |
|
| 404 |
|
|
|
|
| 425 |
{"n_lists": len(LISTS), "attention_checks": CHECKS, "not_in_study": NOT_IN_STUDY,
|
| 426 |
"exposure": dict(sorted(EXPOSURE.items())), "lists": LISTS,
|
| 427 |
"ranking_sets": RANKING_SETS, "list_rankings": LIST_RANKINGS}, indent=2))
|
| 428 |
+
print(f"{len(LISTS)} lists of {sorted({len(l) for l in LISTS})} images ({'from the dataset' if all(isinstance(s.get('list'), int) for s in STUDY) else 'built here'}), "
|
| 429 |
+
f"{sorted({sum(i in CHECKS for i in l) for l in LISTS})} attention checks each; {len(NOT_IN_STUDY)} stimuli not in the study (human_study = false); "
|
| 430 |
f"{len(RANKING_SETS)} ranking groups, {len(LIST_RANKINGS[0])} per list")
|
| 431 |
|
| 432 |
|