bs82 commited on
Commit
a9d7edb
·
verified ·
1 Parent(s): c756bb5

deploy 4-style study

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +160 -0
  2. Dockerfile +6 -0
  3. README.md +5 -5
  4. app/main.py +187 -0
  5. app/static/imgs/0000_L.png +3 -0
  6. app/static/imgs/0000_R.png +3 -0
  7. app/static/imgs/0001_L.png +3 -0
  8. app/static/imgs/0001_R.png +3 -0
  9. app/static/imgs/0002_L.png +3 -0
  10. app/static/imgs/0002_R.png +3 -0
  11. app/static/imgs/0003_L.png +3 -0
  12. app/static/imgs/0003_R.png +3 -0
  13. app/static/imgs/0004_L.png +3 -0
  14. app/static/imgs/0004_R.png +3 -0
  15. app/static/imgs/0005_L.png +3 -0
  16. app/static/imgs/0005_R.png +3 -0
  17. app/static/imgs/0006_L.png +3 -0
  18. app/static/imgs/0006_R.png +3 -0
  19. app/static/imgs/0007_L.png +3 -0
  20. app/static/imgs/0007_R.png +3 -0
  21. app/static/imgs/0008_L.png +3 -0
  22. app/static/imgs/0008_R.png +3 -0
  23. app/static/imgs/0009_L.png +3 -0
  24. app/static/imgs/0009_R.png +3 -0
  25. app/static/imgs/0010_L.png +3 -0
  26. app/static/imgs/0010_R.png +3 -0
  27. app/static/imgs/0011_L.png +3 -0
  28. app/static/imgs/0011_R.png +3 -0
  29. app/static/imgs/0012_L.png +3 -0
  30. app/static/imgs/0012_R.png +3 -0
  31. app/static/imgs/0013_L.png +3 -0
  32. app/static/imgs/0013_R.png +3 -0
  33. app/static/imgs/0014_L.png +3 -0
  34. app/static/imgs/0014_R.png +3 -0
  35. app/static/imgs/0015_L.png +3 -0
  36. app/static/imgs/0015_R.png +3 -0
  37. app/static/imgs/0016_L.png +3 -0
  38. app/static/imgs/0016_R.png +3 -0
  39. app/static/imgs/0017_L.png +3 -0
  40. app/static/imgs/0017_R.png +3 -0
  41. app/static/imgs/0018_L.png +3 -0
  42. app/static/imgs/0018_R.png +3 -0
  43. app/static/imgs/0019_L.png +3 -0
  44. app/static/imgs/0019_R.png +3 -0
  45. app/static/imgs/0020_L.png +3 -0
  46. app/static/imgs/0020_R.png +3 -0
  47. app/static/imgs/0021_L.png +3 -0
  48. app/static/imgs/0021_R.png +3 -0
  49. app/static/imgs/0022_L.png +3 -0
  50. app/static/imgs/0022_R.png +3 -0
.gitattributes CHANGED
@@ -33,3 +33,163 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ app/static/imgs/0000_L.png filter=lfs diff=lfs merge=lfs -text
37
+ app/static/imgs/0000_R.png filter=lfs diff=lfs merge=lfs -text
38
+ app/static/imgs/0001_L.png filter=lfs diff=lfs merge=lfs -text
39
+ app/static/imgs/0001_R.png filter=lfs diff=lfs merge=lfs -text
40
+ app/static/imgs/0002_L.png filter=lfs diff=lfs merge=lfs -text
41
+ app/static/imgs/0002_R.png filter=lfs diff=lfs merge=lfs -text
42
+ app/static/imgs/0003_L.png filter=lfs diff=lfs merge=lfs -text
43
+ app/static/imgs/0003_R.png filter=lfs diff=lfs merge=lfs -text
44
+ app/static/imgs/0004_L.png filter=lfs diff=lfs merge=lfs -text
45
+ app/static/imgs/0004_R.png filter=lfs diff=lfs merge=lfs -text
46
+ app/static/imgs/0005_L.png filter=lfs diff=lfs merge=lfs -text
47
+ app/static/imgs/0005_R.png filter=lfs diff=lfs merge=lfs -text
48
+ app/static/imgs/0006_L.png filter=lfs diff=lfs merge=lfs -text
49
+ app/static/imgs/0006_R.png filter=lfs diff=lfs merge=lfs -text
50
+ app/static/imgs/0007_L.png filter=lfs diff=lfs merge=lfs -text
51
+ app/static/imgs/0007_R.png filter=lfs diff=lfs merge=lfs -text
52
+ app/static/imgs/0008_L.png filter=lfs diff=lfs merge=lfs -text
53
+ app/static/imgs/0008_R.png filter=lfs diff=lfs merge=lfs -text
54
+ app/static/imgs/0009_L.png filter=lfs diff=lfs merge=lfs -text
55
+ app/static/imgs/0009_R.png filter=lfs diff=lfs merge=lfs -text
56
+ app/static/imgs/0010_L.png filter=lfs diff=lfs merge=lfs -text
57
+ app/static/imgs/0010_R.png filter=lfs diff=lfs merge=lfs -text
58
+ app/static/imgs/0011_L.png filter=lfs diff=lfs merge=lfs -text
59
+ app/static/imgs/0011_R.png filter=lfs diff=lfs merge=lfs -text
60
+ app/static/imgs/0012_L.png filter=lfs diff=lfs merge=lfs -text
61
+ app/static/imgs/0012_R.png filter=lfs diff=lfs merge=lfs -text
62
+ app/static/imgs/0013_L.png filter=lfs diff=lfs merge=lfs -text
63
+ app/static/imgs/0013_R.png filter=lfs diff=lfs merge=lfs -text
64
+ app/static/imgs/0014_L.png filter=lfs diff=lfs merge=lfs -text
65
+ app/static/imgs/0014_R.png filter=lfs diff=lfs merge=lfs -text
66
+ app/static/imgs/0015_L.png filter=lfs diff=lfs merge=lfs -text
67
+ app/static/imgs/0015_R.png filter=lfs diff=lfs merge=lfs -text
68
+ app/static/imgs/0016_L.png filter=lfs diff=lfs merge=lfs -text
69
+ app/static/imgs/0016_R.png filter=lfs diff=lfs merge=lfs -text
70
+ app/static/imgs/0017_L.png filter=lfs diff=lfs merge=lfs -text
71
+ app/static/imgs/0017_R.png filter=lfs diff=lfs merge=lfs -text
72
+ app/static/imgs/0018_L.png filter=lfs diff=lfs merge=lfs -text
73
+ app/static/imgs/0018_R.png filter=lfs diff=lfs merge=lfs -text
74
+ app/static/imgs/0019_L.png filter=lfs diff=lfs merge=lfs -text
75
+ app/static/imgs/0019_R.png filter=lfs diff=lfs merge=lfs -text
76
+ app/static/imgs/0020_L.png filter=lfs diff=lfs merge=lfs -text
77
+ app/static/imgs/0020_R.png filter=lfs diff=lfs merge=lfs -text
78
+ app/static/imgs/0021_L.png filter=lfs diff=lfs merge=lfs -text
79
+ app/static/imgs/0021_R.png filter=lfs diff=lfs merge=lfs -text
80
+ app/static/imgs/0022_L.png filter=lfs diff=lfs merge=lfs -text
81
+ app/static/imgs/0022_R.png filter=lfs diff=lfs merge=lfs -text
82
+ app/static/imgs/0023_L.png filter=lfs diff=lfs merge=lfs -text
83
+ app/static/imgs/0023_R.png filter=lfs diff=lfs merge=lfs -text
84
+ app/static/imgs/0024_L.png filter=lfs diff=lfs merge=lfs -text
85
+ app/static/imgs/0024_R.png filter=lfs diff=lfs merge=lfs -text
86
+ app/static/imgs/0025_L.png filter=lfs diff=lfs merge=lfs -text
87
+ app/static/imgs/0025_R.png filter=lfs diff=lfs merge=lfs -text
88
+ app/static/imgs/0026_L.png filter=lfs diff=lfs merge=lfs -text
89
+ app/static/imgs/0026_R.png filter=lfs diff=lfs merge=lfs -text
90
+ app/static/imgs/0027_L.png filter=lfs diff=lfs merge=lfs -text
91
+ app/static/imgs/0027_R.png filter=lfs diff=lfs merge=lfs -text
92
+ app/static/imgs/0028_L.png filter=lfs diff=lfs merge=lfs -text
93
+ app/static/imgs/0028_R.png filter=lfs diff=lfs merge=lfs -text
94
+ app/static/imgs/0029_L.png filter=lfs diff=lfs merge=lfs -text
95
+ app/static/imgs/0029_R.png filter=lfs diff=lfs merge=lfs -text
96
+ app/static/imgs/0030_L.png filter=lfs diff=lfs merge=lfs -text
97
+ app/static/imgs/0030_R.png filter=lfs diff=lfs merge=lfs -text
98
+ app/static/imgs/0031_L.png filter=lfs diff=lfs merge=lfs -text
99
+ app/static/imgs/0031_R.png filter=lfs diff=lfs merge=lfs -text
100
+ app/static/imgs/0032_L.png filter=lfs diff=lfs merge=lfs -text
101
+ app/static/imgs/0032_R.png filter=lfs diff=lfs merge=lfs -text
102
+ app/static/imgs/0033_L.png filter=lfs diff=lfs merge=lfs -text
103
+ app/static/imgs/0033_R.png filter=lfs diff=lfs merge=lfs -text
104
+ app/static/imgs/0034_L.png filter=lfs diff=lfs merge=lfs -text
105
+ app/static/imgs/0034_R.png filter=lfs diff=lfs merge=lfs -text
106
+ app/static/imgs/0035_L.png filter=lfs diff=lfs merge=lfs -text
107
+ app/static/imgs/0035_R.png filter=lfs diff=lfs merge=lfs -text
108
+ app/static/imgs/0036_L.png filter=lfs diff=lfs merge=lfs -text
109
+ app/static/imgs/0036_R.png filter=lfs diff=lfs merge=lfs -text
110
+ app/static/imgs/0037_L.png filter=lfs diff=lfs merge=lfs -text
111
+ app/static/imgs/0037_R.png filter=lfs diff=lfs merge=lfs -text
112
+ app/static/imgs/0038_L.png filter=lfs diff=lfs merge=lfs -text
113
+ app/static/imgs/0038_R.png filter=lfs diff=lfs merge=lfs -text
114
+ app/static/imgs/0039_L.png filter=lfs diff=lfs merge=lfs -text
115
+ app/static/imgs/0039_R.png filter=lfs diff=lfs merge=lfs -text
116
+ app/static/imgs/0040_L.png filter=lfs diff=lfs merge=lfs -text
117
+ app/static/imgs/0040_R.png filter=lfs diff=lfs merge=lfs -text
118
+ app/static/imgs/0041_L.png filter=lfs diff=lfs merge=lfs -text
119
+ app/static/imgs/0041_R.png filter=lfs diff=lfs merge=lfs -text
120
+ app/static/imgs/0042_L.png filter=lfs diff=lfs merge=lfs -text
121
+ app/static/imgs/0042_R.png filter=lfs diff=lfs merge=lfs -text
122
+ app/static/imgs/0043_L.png filter=lfs diff=lfs merge=lfs -text
123
+ app/static/imgs/0043_R.png filter=lfs diff=lfs merge=lfs -text
124
+ app/static/imgs/0044_L.png filter=lfs diff=lfs merge=lfs -text
125
+ app/static/imgs/0044_R.png filter=lfs diff=lfs merge=lfs -text
126
+ app/static/imgs/0045_L.png filter=lfs diff=lfs merge=lfs -text
127
+ app/static/imgs/0045_R.png filter=lfs diff=lfs merge=lfs -text
128
+ app/static/imgs/0046_L.png filter=lfs diff=lfs merge=lfs -text
129
+ app/static/imgs/0046_R.png filter=lfs diff=lfs merge=lfs -text
130
+ app/static/imgs/0047_L.png filter=lfs diff=lfs merge=lfs -text
131
+ app/static/imgs/0047_R.png filter=lfs diff=lfs merge=lfs -text
132
+ app/static/imgs/0048_L.png filter=lfs diff=lfs merge=lfs -text
133
+ app/static/imgs/0048_R.png filter=lfs diff=lfs merge=lfs -text
134
+ app/static/imgs/0049_L.png filter=lfs diff=lfs merge=lfs -text
135
+ app/static/imgs/0049_R.png filter=lfs diff=lfs merge=lfs -text
136
+ app/static/imgs/0050_L.png filter=lfs diff=lfs merge=lfs -text
137
+ app/static/imgs/0050_R.png filter=lfs diff=lfs merge=lfs -text
138
+ app/static/imgs/0051_L.png filter=lfs diff=lfs merge=lfs -text
139
+ app/static/imgs/0051_R.png filter=lfs diff=lfs merge=lfs -text
140
+ app/static/imgs/0052_L.png filter=lfs diff=lfs merge=lfs -text
141
+ app/static/imgs/0052_R.png filter=lfs diff=lfs merge=lfs -text
142
+ app/static/imgs/0053_L.png filter=lfs diff=lfs merge=lfs -text
143
+ app/static/imgs/0053_R.png filter=lfs diff=lfs merge=lfs -text
144
+ app/static/imgs/0054_L.png filter=lfs diff=lfs merge=lfs -text
145
+ app/static/imgs/0054_R.png filter=lfs diff=lfs merge=lfs -text
146
+ app/static/imgs/0055_L.png filter=lfs diff=lfs merge=lfs -text
147
+ app/static/imgs/0055_R.png filter=lfs diff=lfs merge=lfs -text
148
+ app/static/imgs/0056_L.png filter=lfs diff=lfs merge=lfs -text
149
+ app/static/imgs/0056_R.png filter=lfs diff=lfs merge=lfs -text
150
+ app/static/imgs/0057_L.png filter=lfs diff=lfs merge=lfs -text
151
+ app/static/imgs/0057_R.png filter=lfs diff=lfs merge=lfs -text
152
+ app/static/imgs/0058_L.png filter=lfs diff=lfs merge=lfs -text
153
+ app/static/imgs/0058_R.png filter=lfs diff=lfs merge=lfs -text
154
+ app/static/imgs/0059_L.png filter=lfs diff=lfs merge=lfs -text
155
+ app/static/imgs/0059_R.png filter=lfs diff=lfs merge=lfs -text
156
+ app/static/imgs/0060_L.png filter=lfs diff=lfs merge=lfs -text
157
+ app/static/imgs/0060_R.png filter=lfs diff=lfs merge=lfs -text
158
+ app/static/imgs/0061_L.png filter=lfs diff=lfs merge=lfs -text
159
+ app/static/imgs/0061_R.png filter=lfs diff=lfs merge=lfs -text
160
+ app/static/imgs/0062_L.png filter=lfs diff=lfs merge=lfs -text
161
+ app/static/imgs/0062_R.png filter=lfs diff=lfs merge=lfs -text
162
+ app/static/imgs/0063_L.png filter=lfs diff=lfs merge=lfs -text
163
+ app/static/imgs/0063_R.png filter=lfs diff=lfs merge=lfs -text
164
+ app/static/imgs/0064_L.png filter=lfs diff=lfs merge=lfs -text
165
+ app/static/imgs/0064_R.png filter=lfs diff=lfs merge=lfs -text
166
+ app/static/imgs/0065_L.png filter=lfs diff=lfs merge=lfs -text
167
+ app/static/imgs/0065_R.png filter=lfs diff=lfs merge=lfs -text
168
+ app/static/imgs/0066_L.png filter=lfs diff=lfs merge=lfs -text
169
+ app/static/imgs/0066_R.png filter=lfs diff=lfs merge=lfs -text
170
+ app/static/imgs/0067_L.png filter=lfs diff=lfs merge=lfs -text
171
+ app/static/imgs/0067_R.png filter=lfs diff=lfs merge=lfs -text
172
+ app/static/imgs/0068_L.png filter=lfs diff=lfs merge=lfs -text
173
+ app/static/imgs/0068_R.png filter=lfs diff=lfs merge=lfs -text
174
+ app/static/imgs/0069_L.png filter=lfs diff=lfs merge=lfs -text
175
+ app/static/imgs/0069_R.png filter=lfs diff=lfs merge=lfs -text
176
+ app/static/imgs/0070_L.png filter=lfs diff=lfs merge=lfs -text
177
+ app/static/imgs/0070_R.png filter=lfs diff=lfs merge=lfs -text
178
+ app/static/imgs/0071_L.png filter=lfs diff=lfs merge=lfs -text
179
+ app/static/imgs/0071_R.png filter=lfs diff=lfs merge=lfs -text
180
+ app/static/imgs/0072_L.png filter=lfs diff=lfs merge=lfs -text
181
+ app/static/imgs/0072_R.png filter=lfs diff=lfs merge=lfs -text
182
+ app/static/imgs/0073_L.png filter=lfs diff=lfs merge=lfs -text
183
+ app/static/imgs/0073_R.png filter=lfs diff=lfs merge=lfs -text
184
+ app/static/imgs/0074_L.png filter=lfs diff=lfs merge=lfs -text
185
+ app/static/imgs/0074_R.png filter=lfs diff=lfs merge=lfs -text
186
+ app/static/imgs/0075_L.png filter=lfs diff=lfs merge=lfs -text
187
+ app/static/imgs/0075_R.png filter=lfs diff=lfs merge=lfs -text
188
+ app/static/imgs/0076_L.png filter=lfs diff=lfs merge=lfs -text
189
+ app/static/imgs/0076_R.png filter=lfs diff=lfs merge=lfs -text
190
+ app/static/imgs/0077_L.png filter=lfs diff=lfs merge=lfs -text
191
+ app/static/imgs/0077_R.png filter=lfs diff=lfs merge=lfs -text
192
+ app/static/imgs/0078_L.png filter=lfs diff=lfs merge=lfs -text
193
+ app/static/imgs/0078_R.png filter=lfs diff=lfs merge=lfs -text
194
+ app/static/imgs/0079_L.png filter=lfs diff=lfs merge=lfs -text
195
+ app/static/imgs/0079_R.png filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ COPY requirements.txt .
4
+ RUN pip install --no-cache-dir -r requirements.txt
5
+ COPY . .
6
+ CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,10 @@
1
  ---
2
  title: Image Preference Study
3
- emoji: 💻
4
- colorFrom: purple
5
- colorTo: green
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Image Preference Study
3
+ emoji: 🖼️
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
+ A short 2-alternative forced-choice image preference study.
 
app/main.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """Human-preference study (2AFC pairwise) — SD3.5-Medium ImageReward-guided vs vanilla, 4 styles.
3
+ Accepted T2I protocol (Imagen/Parti/Emu/Diffusion-DPO): two questions/pair (overall preference + prompt
4
+ alignment), tie option, neutral labels, left/right randomized, gold catch trials, email-gated (one
5
+ submission/person). SQLite backend, JSON API + single-page UI.
6
+
7
+ Run: ./env/bin/python -m uvicorn human_study.app.main:app --host 0.0.0.0 --port 8000
8
+ DB: human_study/data/study.db Manifest: human_study/data/pairs.json
9
+ Admin: GET /api/stats?key=$STUDY_ADMIN_KEY GET /api/export?key=... (CSV)
10
+ """
11
+ import json, os, sqlite3, time, uuid, random, csv, io, shutil
12
+ from pathlib import Path
13
+ from fastapi import FastAPI, Request, HTTPException
14
+ from fastapi.responses import HTMLResponse, PlainTextResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+
17
+ HERE = Path(__file__).resolve().parent
18
+ # STUDY_DATA (pairs.json + study.db) and STUDY_STATIC (index.html + imgs/) are env-overridable so the same
19
+ # app code can host multiple independent studies (e.g. the 4-style study and a photo-only study).
20
+ DATA = Path(os.environ.get("STUDY_DATA", str(HERE.parent / "data")))
21
+ STATIC = Path(os.environ.get("STUDY_STATIC", str(HERE / "static")))
22
+ DB = DATA / "study.db"
23
+ PAIRS = {p["pair_id"]: p for p in json.load(open(DATA / "pairs.json"))}
24
+ ADMIN_KEY = os.environ.get("STUDY_ADMIN_KEY", "rsfg-admin")
25
+
26
+ # Durable storage on Hugging Face Spaces (ephemeral disk): restore the response DB from a private Dataset on
27
+ # startup, back it up after every submission. No-op when the env vars are unset (e.g. local runs).
28
+ PERSIST_REPO = os.environ.get("HF_PERSIST_REPO") # e.g. "bs82/image-study-db"
29
+ STUDY_NAME = os.environ.get("STUDY_NAME", "study") # subfolder in the Dataset (one per study)
30
+ HF_TOKEN = os.environ.get("HF_TOKEN")
31
+ _DB_IN_REPO = f"{STUDY_NAME}/study.db"
32
+
33
+ def restore_db():
34
+ if not (PERSIST_REPO and HF_TOKEN):
35
+ return
36
+ try:
37
+ from huggingface_hub import hf_hub_download
38
+ p = hf_hub_download(PERSIST_REPO, _DB_IN_REPO, repo_type="dataset", token=HF_TOKEN)
39
+ shutil.copy(p, DB)
40
+ print(f"[persist] restored DB from {PERSIST_REPO}:{_DB_IN_REPO}")
41
+ except Exception as e:
42
+ print(f"[persist] no prior DB ({e}); starting fresh")
43
+
44
+ def backup_db():
45
+ if not (PERSIST_REPO and HF_TOKEN):
46
+ return
47
+ try:
48
+ from huggingface_hub import HfApi
49
+ HfApi(token=HF_TOKEN).upload_file(path_or_fileobj=str(DB), path_in_repo=_DB_IN_REPO,
50
+ repo_id=PERSIST_REPO, repo_type="dataset",
51
+ commit_message=f"backup {STUDY_NAME}")
52
+ except Exception as e:
53
+ print(f"[persist] backup failed: {e}")
54
+
55
+ def db():
56
+ c = sqlite3.connect(DB); c.row_factory = sqlite3.Row; return c
57
+
58
+ def init_db():
59
+ DATA.mkdir(parents=True, exist_ok=True)
60
+ with db() as c:
61
+ c.execute("""CREATE TABLE IF NOT EXISTS participants(
62
+ token TEXT PRIMARY KEY, email TEXT UNIQUE, created REAL, completed INTEGER DEFAULT 0,
63
+ n_catch INTEGER DEFAULT 0, n_catch_pass INTEGER DEFAULT 0, record INTEGER DEFAULT 1)""")
64
+ c.execute("""CREATE TABLE IF NOT EXISTS votes(
65
+ token TEXT, email TEXT, pair_id INTEGER, kind TEXT, style TEXT, question TEXT,
66
+ choice TEXT, guided_chosen INTEGER, ts REAL)""") # guided_chosen: 1 guided, 0 vanilla, NULL tie
67
+ restore_db()
68
+ init_db()
69
+
70
+ app = FastAPI(title="Image preference study")
71
+ app.mount("/static", StaticFiles(directory=str(STATIC)), name="static")
72
+
73
+ @app.get("/", response_class=HTMLResponse)
74
+ def index():
75
+ return (STATIC / "index.html").read_text()
76
+
77
+ @app.get("/api/info")
78
+ def info():
79
+ n = len(PAIRS)
80
+ return {"n_pairs": n, "est_minutes": max(1, round(n * 3.5 / 60))}
81
+
82
+ def norm_email(e):
83
+ e = (e or "").strip().lower()
84
+ if "@" not in e or "." not in e.split("@")[-1] or len(e) < 5:
85
+ raise HTTPException(400, "Please enter a valid email address.")
86
+ return e
87
+
88
+ @app.post("/api/register")
89
+ async def register(req: Request):
90
+ body = await req.json()
91
+ email = norm_email(body.get("email"))
92
+ record = 0 if body.get("record") is False else 1 # opt-out checkbox; default = record
93
+ with db() as c:
94
+ row = c.execute("SELECT token, completed FROM participants WHERE email=?", (email,)).fetchone()
95
+ if row and row["completed"]:
96
+ raise HTTPException(409, "This email has already completed the study. Thank you!")
97
+ token = row["token"] if row else uuid.uuid4().hex
98
+ if not row:
99
+ c.execute("INSERT INTO participants(token,email,created,record) VALUES(?,?,?,?)",
100
+ (token, email, time.time(), record))
101
+ else:
102
+ c.execute("UPDATE participants SET record=? WHERE token=?", (record, token))
103
+ order = list(PAIRS.values())
104
+ random.Random(hash(token) & 0xffffffff).shuffle(order)
105
+ pairs = [{"pair_id": p["pair_id"], "prompt": p["prompt"],
106
+ "left": f"/static/imgs/{p['left']}", "right": f"/static/imgs/{p['right']}"} for p in order]
107
+ return {"token": token, "pairs": pairs}
108
+
109
+ def _summarize(votes):
110
+ """Per-participant own results (thank-you page): one-click guided-preference counts overall + per style."""
111
+ overall = [0, 0]; by_style = {}
112
+ for v in votes:
113
+ p = PAIRS.get(int(v.get("pair_id", -1)))
114
+ ch = v.get("choice")
115
+ if not p or p["kind"] != "real" or ch not in ("left", "right"):
116
+ continue
117
+ gw = int(ch == p["guided_side"])
118
+ overall[0] += gw; overall[1] += 1
119
+ s = by_style.setdefault(p["style"], [0, 0]); s[0] += gw; s[1] += 1
120
+ return {"pref": {"guided": overall[0], "n": overall[1],
121
+ "rate": (overall[0] / overall[1]) if overall[1] else None,
122
+ "by_style": {k: {"guided": x[0], "n": x[1], "rate": (x[0] / x[1]) if x[1] else None}
123
+ for k, x in by_style.items()}}}
124
+
125
+ @app.post("/api/submit")
126
+ async def submit(req: Request):
127
+ body = await req.json()
128
+ token, votes = body.get("token", ""), body.get("votes", [])
129
+ with db() as c:
130
+ row = c.execute("SELECT email, completed, record FROM participants WHERE token=?", (token,)).fetchone()
131
+ if not row:
132
+ raise HTTPException(404, "Unknown session.")
133
+ if row["completed"]:
134
+ raise HTTPException(409, "Already submitted.")
135
+ email, record = row["email"], row["record"]
136
+ n_catch = n_pass = 0
137
+ for v in votes:
138
+ p = PAIRS.get(int(v.get("pair_id", -1)))
139
+ ch = v.get("choice")
140
+ if not p or ch not in ("left", "right"):
141
+ continue
142
+ if p["kind"] == "catch":
143
+ gc = int(ch == p["correct_side"]); n_catch += 1; n_pass += gc
144
+ else:
145
+ gc = int(ch == p["guided_side"])
146
+ if record: # opt-out: skip persisting votes (results still shown to the participant)
147
+ c.execute("INSERT INTO votes VALUES(?,?,?,?,?,?,?,?,?)",
148
+ (token, email, p["pair_id"], p["kind"], p["style"], "pref", ch, gc, time.time()))
149
+ c.execute("UPDATE participants SET completed=1, n_catch=?, n_catch_pass=? WHERE token=?",
150
+ (n_catch, n_pass, token))
151
+ backup_db()
152
+ return {"ok": True, "recorded": bool(record), "results": _summarize(votes)}
153
+
154
+ @app.get("/api/stats")
155
+ def stats(key: str = ""):
156
+ if key != ADMIN_KEY:
157
+ raise HTTPException(403, "bad key")
158
+ with db() as c:
159
+ np_ = c.execute("SELECT COUNT(*) n FROM participants WHERE completed=1").fetchone()["n"]
160
+ # attentive = passed all catch preference trials
161
+ attn = "token IN (SELECT token FROM participants WHERE completed=1 AND n_catch_pass=n_catch AND n_catch>0)"
162
+ out = {"completed_participants": np_, "by_question": {}}
163
+ for q in ("pref", "align"):
164
+ rows = c.execute(f"""SELECT style, COUNT(*) n, SUM(guided_chosen) g,
165
+ SUM(CASE WHEN guided_chosen IS NULL THEN 1 ELSE 0 END) ties
166
+ FROM votes WHERE kind='real' AND question=? AND token IN
167
+ (SELECT token FROM participants WHERE completed=1) GROUP BY style""", (q,)).fetchall()
168
+ tot = c.execute(f"""SELECT COUNT(*) n, SUM(guided_chosen) g FROM votes
169
+ WHERE kind='real' AND question=? AND guided_chosen IS NOT NULL
170
+ AND token IN (SELECT token FROM participants WHERE completed=1)""", (q,)).fetchone()
171
+ out["by_question"][q] = {
172
+ "overall_n_nontie": tot["n"], "overall_guided_wins": tot["g"],
173
+ "overall_guided_winrate": (tot["g"]/tot["n"]) if tot["n"] else None,
174
+ "by_style": {r["style"]: {"n": r["n"], "guided": r["g"], "ties": r["ties"]} for r in rows}}
175
+ return out
176
+
177
+ @app.get("/api/export")
178
+ def export(key: str = ""):
179
+ if key != ADMIN_KEY:
180
+ raise HTTPException(403, "bad key")
181
+ buf = io.StringIO(); w = csv.writer(buf)
182
+ w.writerow(["email", "pair_id", "kind", "style", "question", "choice", "guided_chosen", "ts"])
183
+ with db() as c:
184
+ for r in c.execute("SELECT email,pair_id,kind,style,question,choice,guided_chosen,ts FROM votes ORDER BY ts"):
185
+ w.writerow([r["email"], r["pair_id"], r["kind"], r["style"], r["question"], r["choice"],
186
+ "" if r["guided_chosen"] is None else r["guided_chosen"], r["ts"]])
187
+ return PlainTextResponse(buf.getvalue(), media_type="text/csv")
app/static/imgs/0000_L.png ADDED

Git LFS Details

  • SHA256: 38c60a9c4a347e046f994335cd3896359d5ae91420f9c6459a4278c8ec964531
  • Pointer size: 131 Bytes
  • Size of remote file: 526 kB
app/static/imgs/0000_R.png ADDED

Git LFS Details

  • SHA256: 6930ef9e8ed7e2e3c0d9cecaa068736ef781e9b93ba08d89b5377fc6c65b3d94
  • Pointer size: 131 Bytes
  • Size of remote file: 441 kB
app/static/imgs/0001_L.png ADDED

Git LFS Details

  • SHA256: a66ff2b418e7889f6cc1dfec924d8ecfdcda56848d362ddb2aea0c3022174339
  • Pointer size: 131 Bytes
  • Size of remote file: 380 kB
app/static/imgs/0001_R.png ADDED

Git LFS Details

  • SHA256: 867d7deea73d925909d3e4a9eb2369e9f79592d468153ff65f0606d89953764e
  • Pointer size: 131 Bytes
  • Size of remote file: 462 kB
app/static/imgs/0002_L.png ADDED

Git LFS Details

  • SHA256: 9b541e09d17df2fab2f7597f15165c049a138961ca35cc77ce5f38f33fa0ff04
  • Pointer size: 131 Bytes
  • Size of remote file: 433 kB
app/static/imgs/0002_R.png ADDED

Git LFS Details

  • SHA256: 9e1221058fb988bd917a1d15f73d6afb4e4549941cac611bafe95268bf8de844
  • Pointer size: 131 Bytes
  • Size of remote file: 341 kB
app/static/imgs/0003_L.png ADDED

Git LFS Details

  • SHA256: 0d14a9255e26bee8910fbc71a7384bc2414d2430fe1e33fc655766d8272c24ee
  • Pointer size: 131 Bytes
  • Size of remote file: 534 kB
app/static/imgs/0003_R.png ADDED

Git LFS Details

  • SHA256: dc4a9d83b3bbb9eedf22aaddb27d795c4a1d5f8cdffbf829edb66157475dfa17
  • Pointer size: 131 Bytes
  • Size of remote file: 565 kB
app/static/imgs/0004_L.png ADDED

Git LFS Details

  • SHA256: dcf57bdc2bf564fe8cd1a63485fd51623a0af9a95dead44ec66a4e11f5dddc29
  • Pointer size: 131 Bytes
  • Size of remote file: 396 kB
app/static/imgs/0004_R.png ADDED

Git LFS Details

  • SHA256: 97deafe740536058ace6675114bc772e3fea7d9a7d1b17d6af867dfb294855dd
  • Pointer size: 131 Bytes
  • Size of remote file: 334 kB
app/static/imgs/0005_L.png ADDED

Git LFS Details

  • SHA256: 9a7d24ebf9b6a3d2c0c91c894dd94c6c10099bb638531de8b6b3ad1a20fa2d74
  • Pointer size: 131 Bytes
  • Size of remote file: 393 kB
app/static/imgs/0005_R.png ADDED

Git LFS Details

  • SHA256: 5199500d553255242fdb7297e7f040a3925512757d21f1816dbabbad82ba1a24
  • Pointer size: 131 Bytes
  • Size of remote file: 294 kB
app/static/imgs/0006_L.png ADDED

Git LFS Details

  • SHA256: 20a50fee489f5936f159abaa53b92910993df23f8ac29ad5103e707d0cad600d
  • Pointer size: 131 Bytes
  • Size of remote file: 411 kB
app/static/imgs/0006_R.png ADDED

Git LFS Details

  • SHA256: 0b1499297d74f8e12eeddb876df3ae088231a5664ee1cab5de80fe3dc0e4aa67
  • Pointer size: 131 Bytes
  • Size of remote file: 337 kB
app/static/imgs/0007_L.png ADDED

Git LFS Details

  • SHA256: a9b2f391046a5f65252ccfe5ed2a0bbac9eb81f5507ec8a4f3c888e179321591
  • Pointer size: 131 Bytes
  • Size of remote file: 310 kB
app/static/imgs/0007_R.png ADDED

Git LFS Details

  • SHA256: 84362be8ebeb7f6d00bda0596cbc0c62600f1cf01facf8d264401cc99d77cb23
  • Pointer size: 131 Bytes
  • Size of remote file: 346 kB
app/static/imgs/0008_L.png ADDED

Git LFS Details

  • SHA256: da66fae506324726496eb9fadf817c7d41b25adafe952909e36541942bb8f57c
  • Pointer size: 131 Bytes
  • Size of remote file: 440 kB
app/static/imgs/0008_R.png ADDED

Git LFS Details

  • SHA256: a190f8e920fc3a391bbdd7566adb28a53b751006294344688dccfe5dbe6a2778
  • Pointer size: 131 Bytes
  • Size of remote file: 353 kB
app/static/imgs/0009_L.png ADDED

Git LFS Details

  • SHA256: cbe42f24cf185c704523ca26992b9c5f29da81ab4da9b7bda0290103f4fdd2f9
  • Pointer size: 131 Bytes
  • Size of remote file: 344 kB
app/static/imgs/0009_R.png ADDED

Git LFS Details

  • SHA256: bb522416860d37a075a3720f26506687b17a1fba78b21380f8c3c5fe90fea05b
  • Pointer size: 131 Bytes
  • Size of remote file: 453 kB
app/static/imgs/0010_L.png ADDED

Git LFS Details

  • SHA256: 7f7625fb446022ce5ce7573902b45219576dfdaa68dcb9f3f456f87d1f89d78d
  • Pointer size: 131 Bytes
  • Size of remote file: 440 kB
app/static/imgs/0010_R.png ADDED

Git LFS Details

  • SHA256: 328eb6ff5e40b6d6ce9fc66e224820f01fdf020f6639a1b68daa09f14c103ef7
  • Pointer size: 131 Bytes
  • Size of remote file: 534 kB
app/static/imgs/0011_L.png ADDED

Git LFS Details

  • SHA256: c2fef151996cd91404827bdd57327afe1acf21c143887e2c32ec8e55364a14e3
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
app/static/imgs/0011_R.png ADDED

Git LFS Details

  • SHA256: 61919341372257bc62405f82c79b74be824b10293c83def83729a3e560ab5fcd
  • Pointer size: 131 Bytes
  • Size of remote file: 323 kB
app/static/imgs/0012_L.png ADDED

Git LFS Details

  • SHA256: 688b8ff6bbe6c66a3e7ac4581e241d32cc96e27fd3b4f6d49e2c860adff74c8b
  • Pointer size: 131 Bytes
  • Size of remote file: 635 kB
app/static/imgs/0012_R.png ADDED

Git LFS Details

  • SHA256: 79ba7d60c159d1604b03a3fe8ffd46452800365658450a9983327d708058b6bb
  • Pointer size: 131 Bytes
  • Size of remote file: 606 kB
app/static/imgs/0013_L.png ADDED

Git LFS Details

  • SHA256: 4c525a86350a3d642c3a5d5716c950f281707e812087990ce3f7ff76b8e23426
  • Pointer size: 131 Bytes
  • Size of remote file: 579 kB
app/static/imgs/0013_R.png ADDED

Git LFS Details

  • SHA256: edb40ef5e78362f404a1d910ef18a56ed7bea0654a99882314b974109c464f8f
  • Pointer size: 131 Bytes
  • Size of remote file: 576 kB
app/static/imgs/0014_L.png ADDED

Git LFS Details

  • SHA256: c65d9307074b168dc129ed44b601c5d8fd3eacf5201e731556bfff0b6b3f012b
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
app/static/imgs/0014_R.png ADDED

Git LFS Details

  • SHA256: 77ef037047ca568bee9ea41050063ff4c868dd6a2bcf408a31e23bde26c400fb
  • Pointer size: 131 Bytes
  • Size of remote file: 377 kB
app/static/imgs/0015_L.png ADDED

Git LFS Details

  • SHA256: 65c9f442f11622e92bc646f38bb95014a55aa65229e721ae0cbd235fadfa15da
  • Pointer size: 131 Bytes
  • Size of remote file: 478 kB
app/static/imgs/0015_R.png ADDED

Git LFS Details

  • SHA256: 2ae01db5751d68f4748eb99a73d972b9f6d730f9cfce3b8c665e151f8a0eac80
  • Pointer size: 131 Bytes
  • Size of remote file: 426 kB
app/static/imgs/0016_L.png ADDED

Git LFS Details

  • SHA256: 214d43fb5b6328790fe21379d1fe8279ccd6ed730a53bd033c9b860cf108a810
  • Pointer size: 131 Bytes
  • Size of remote file: 489 kB
app/static/imgs/0016_R.png ADDED

Git LFS Details

  • SHA256: d00c11b78a403240497a9342cd478781fe6304a6ae9f21b7ecc564e8928b7500
  • Pointer size: 131 Bytes
  • Size of remote file: 394 kB
app/static/imgs/0017_L.png ADDED

Git LFS Details

  • SHA256: a5ba9cb891c7c0c8a42310123f9e100692642344c833b19c6c30d1c442e391f8
  • Pointer size: 131 Bytes
  • Size of remote file: 473 kB
app/static/imgs/0017_R.png ADDED

Git LFS Details

  • SHA256: 8d102c6466ea0ae5c6b71abdca176b0d9058f8ca7d4e15dd15dbdac2dcc39db5
  • Pointer size: 131 Bytes
  • Size of remote file: 412 kB
app/static/imgs/0018_L.png ADDED

Git LFS Details

  • SHA256: b9388ed4632242b987efd6fdbc4e54005b957d7b8bc91a27ae04395f473c073c
  • Pointer size: 131 Bytes
  • Size of remote file: 371 kB
app/static/imgs/0018_R.png ADDED

Git LFS Details

  • SHA256: 8ab69734ab640dd7ed3bc412b6b29da6ca7881f1c7b00de2532ccb5058eaf82f
  • Pointer size: 131 Bytes
  • Size of remote file: 288 kB
app/static/imgs/0019_L.png ADDED

Git LFS Details

  • SHA256: 62f2960b25ee5c0d7d3cc807cd7b1120a160f9d60992b8fde1c38b0ade07365d
  • Pointer size: 131 Bytes
  • Size of remote file: 407 kB
app/static/imgs/0019_R.png ADDED

Git LFS Details

  • SHA256: 4beaccf2f5e0565a3d29bbab60450effabf8fb31aa19681e801a60c24b245810
  • Pointer size: 131 Bytes
  • Size of remote file: 454 kB
app/static/imgs/0020_L.png ADDED

Git LFS Details

  • SHA256: a8ba0db1f7340f476af7871b4a5a9b157b2a12a3111d10fbc303db644de05119
  • Pointer size: 131 Bytes
  • Size of remote file: 421 kB
app/static/imgs/0020_R.png ADDED

Git LFS Details

  • SHA256: 60b9c4892f4f17593473b139ebf67d7df876c921ece89aac825b828ad0e217e0
  • Pointer size: 131 Bytes
  • Size of remote file: 489 kB
app/static/imgs/0021_L.png ADDED

Git LFS Details

  • SHA256: b37882d7c94594284ac2084e30d515a996269375b8a7293cdd261d6d309822c6
  • Pointer size: 131 Bytes
  • Size of remote file: 217 kB
app/static/imgs/0021_R.png ADDED

Git LFS Details

  • SHA256: 44cc4374cf0bd9cbd6fce73271c146d92a034b602bc409fc48b3ab8d66097e37
  • Pointer size: 131 Bytes
  • Size of remote file: 345 kB
app/static/imgs/0022_L.png ADDED

Git LFS Details

  • SHA256: cd97505f62fd9a33f7bae41929327d63b3c03d8e1da4dbd0f2d7a32370fb5a32
  • Pointer size: 131 Bytes
  • Size of remote file: 310 kB
app/static/imgs/0022_R.png ADDED

Git LFS Details

  • SHA256: 00fbbc6272ed8371f98e2ecaa6d224e4057765f0d881e9046cf56c90a504d183
  • Pointer size: 131 Bytes
  • Size of remote file: 410 kB