cgeorgiaw HF Staff commited on
Commit
ffdc611
·
verified ·
1 Parent(s): fe87595

Initial app

Browse files
Files changed (4) hide show
  1. .gitignore +4 -0
  2. README.md +20 -5
  3. app.py +343 -0
  4. requirements.txt +2 -0
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ .gradio/
4
+ flagged/
README.md CHANGED
@@ -1,12 +1,27 @@
1
  ---
2
  title: Denoise Judging
3
- emoji: 🐠
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
  app_file: app.py
9
  pinned: false
 
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Denoise Judging
3
+ emoji: 🔬
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 5.49.1
8
  app_file: app.py
9
  pinned: false
10
+ short_description: Blind A/B judging of denoised microscopy images
11
  ---
12
 
13
+ # Denoise Judging
14
+
15
+ Blind A/B judging Space for the Stemson-AI denoising experiments.
16
+
17
+ - Reads triplets (`raw`, `cimp_gan`, `n2v`) from the private dataset
18
+ `Stemson-AI/denoise-judging-triplets`.
19
+ - Writes one judgment per JSON file to the private dataset
20
+ `Stemson-AI/denoise-judgments`.
21
+ - De-duplicates by email: a user only sees triplets they have not yet judged.
22
+
23
+ ## Required Space secret
24
+
25
+ - `HF_TOKEN` — fine-grained token with **read** access to
26
+ `Stemson-AI/denoise-judging-triplets` and **write** access to
27
+ `Stemson-AI/denoise-judgments`.
app.py ADDED
@@ -0,0 +1,343 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Blind A/B judging Space for denoised images.
2
+
3
+ Reads triplets from a private HF dataset and writes one JSON per judgment
4
+ to a separate private results dataset.
5
+
6
+ Required Space secret: HF_TOKEN (write access to RESULTS_REPO).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ import json
13
+ import os
14
+ import random
15
+ import re
16
+ import uuid
17
+ from datetime import datetime, timezone
18
+ from pathlib import Path
19
+
20
+ import gradio as gr
21
+ from huggingface_hub import HfApi, list_repo_files, snapshot_download
22
+
23
+ TRIPLETS_REPO = "Stemson-AI/denoise-judging-triplets"
24
+ RESULTS_REPO = "Stemson-AI/denoise-judgments"
25
+
26
+ HF_TOKEN = os.environ.get("HF_TOKEN")
27
+ if not HF_TOKEN:
28
+ print("WARNING: HF_TOKEN not set; reads/writes to private repos will fail.")
29
+
30
+ api = HfApi(token=HF_TOKEN)
31
+
32
+ EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
33
+
34
+
35
+ def load_triplets() -> tuple[Path, list[dict]]:
36
+ local = snapshot_download(
37
+ repo_id=TRIPLETS_REPO,
38
+ repo_type="dataset",
39
+ token=HF_TOKEN,
40
+ )
41
+ root = Path(local)
42
+ rows: list[dict] = []
43
+ with open(root / "metadata.jsonl") as f:
44
+ for line in f:
45
+ line = line.strip()
46
+ if line:
47
+ rows.append(json.loads(line))
48
+ return root, rows
49
+
50
+
51
+ def email_slug(email: str) -> str:
52
+ return re.sub(r"[^a-z0-9]+", "_", email.strip().lower()).strip("_")
53
+
54
+
55
+ def already_judged(email: str) -> set[str]:
56
+ """Return triplet_ids the user has already judged, by inspecting filenames."""
57
+ slug = email_slug(email)
58
+ try:
59
+ files = list_repo_files(RESULTS_REPO, repo_type="dataset", token=HF_TOKEN)
60
+ except Exception as exc:
61
+ print(f"list_repo_files failed: {exc!r}")
62
+ return set()
63
+ done: set[str] = set()
64
+ prefix = f"judgments/{slug}__"
65
+ for f in files:
66
+ if not f.startswith(prefix) or not f.endswith(".json"):
67
+ continue
68
+ # judgments/<slug>__<triplet_id>__<ts>.json
69
+ stem = f[len(prefix) : -len(".json")]
70
+ parts = stem.split("__")
71
+ if len(parts) >= 2:
72
+ done.add("__".join(parts[:-1]))
73
+ return done
74
+
75
+
76
+ TRIPLETS_ROOT, TRIPLETS = load_triplets()
77
+ TRIPLET_BY_ID = {r["triplet_id"]: r for r in TRIPLETS}
78
+ print(f"loaded {len(TRIPLETS)} triplets from {TRIPLETS_ROOT}")
79
+
80
+
81
+ # ---------- session helpers ---------------------------------------------------
82
+
83
+
84
+ def _empty_session() -> dict:
85
+ return {
86
+ "name": "",
87
+ "email": "",
88
+ "session_id": "",
89
+ "queue": [], # list of triplet_ids remaining
90
+ "idx": 0, # pointer into queue
91
+ "left_method": "", # which method is shown on the left this turn
92
+ "right_method": "", # which method is shown on the right this turn
93
+ "n_done_now": 0, # judgments made this session
94
+ "n_total": 0, # queue length at session start
95
+ "n_already": 0, # triplet count user had already judged before login
96
+ }
97
+
98
+
99
+ def _paths_for_current(session: dict) -> tuple[str, str, str] | None:
100
+ if session["idx"] >= len(session["queue"]):
101
+ return None
102
+ tid = session["queue"][session["idx"]]
103
+ rec = TRIPLET_BY_ID[tid]
104
+ raw = str(TRIPLETS_ROOT / rec["raw"])
105
+ left = str(TRIPLETS_ROOT / rec[session["left_method"]])
106
+ right = str(TRIPLETS_ROOT / rec[session["right_method"]])
107
+ return raw, left, right
108
+
109
+
110
+ def _assign_sides(session: dict) -> None:
111
+ methods = ["cimp_gan", "n2v"]
112
+ random.shuffle(methods)
113
+ session["left_method"], session["right_method"] = methods
114
+
115
+
116
+ def _progress(session: dict) -> str:
117
+ total = session["n_total"]
118
+ done = session["n_done_now"]
119
+ if total == 0:
120
+ return "All 60 triplets are already judged for this email — thank you!"
121
+ return f"Triplet {min(done + 1, total)} / {total} this session • {session['n_already']} already done before"
122
+
123
+
124
+ # ---------- handlers ----------------------------------------------------------
125
+
126
+
127
+ def start(name: str, email: str):
128
+ name = (name or "").strip()
129
+ email = (email or "").strip().lower()
130
+ if not name:
131
+ return (
132
+ gr.update(), # login_group
133
+ gr.update(), # judging_group
134
+ gr.update(value="Please enter your name.", visible=True), # error_md
135
+ gr.update(), gr.update(), gr.update(), # raw, left, right
136
+ gr.update(), # progress
137
+ _empty_session(),
138
+ gr.update(), gr.update(), # buttons A/B interactivity
139
+ gr.update(), # done_md
140
+ )
141
+ if not EMAIL_RE.match(email):
142
+ return (
143
+ gr.update(),
144
+ gr.update(),
145
+ gr.update(value="Please enter a valid email.", visible=True),
146
+ gr.update(), gr.update(), gr.update(),
147
+ gr.update(),
148
+ _empty_session(),
149
+ gr.update(), gr.update(),
150
+ gr.update(),
151
+ )
152
+
153
+ done = already_judged(email)
154
+ remaining = [r["triplet_id"] for r in TRIPLETS if r["triplet_id"] not in done]
155
+ rng = random.Random(f"{email}|{uuid.uuid4()}")
156
+ rng.shuffle(remaining)
157
+
158
+ session = _empty_session()
159
+ session.update(
160
+ name=name,
161
+ email=email,
162
+ session_id=str(uuid.uuid4()),
163
+ queue=remaining,
164
+ idx=0,
165
+ n_total=len(remaining),
166
+ n_already=len(done),
167
+ )
168
+
169
+ if not remaining:
170
+ return (
171
+ gr.update(visible=False),
172
+ gr.update(visible=True),
173
+ gr.update(visible=False),
174
+ gr.update(value=None), gr.update(value=None), gr.update(value=None),
175
+ gr.update(value=_progress(session)),
176
+ session,
177
+ gr.update(interactive=False), gr.update(interactive=False),
178
+ gr.update(value="Nothing left to judge for this email. Thank you!", visible=True),
179
+ )
180
+
181
+ _assign_sides(session)
182
+ raw, left, right = _paths_for_current(session)
183
+
184
+ return (
185
+ gr.update(visible=False),
186
+ gr.update(visible=True),
187
+ gr.update(visible=False),
188
+ gr.update(value=raw),
189
+ gr.update(value=left),
190
+ gr.update(value=right),
191
+ gr.update(value=_progress(session)),
192
+ session,
193
+ gr.update(interactive=True), gr.update(interactive=True),
194
+ gr.update(visible=False),
195
+ )
196
+
197
+
198
+ def _write_judgment(session: dict, chosen_side: str) -> None:
199
+ tid = session["queue"][session["idx"]]
200
+ chosen_method = (
201
+ session["left_method"] if chosen_side == "A" else session["right_method"]
202
+ )
203
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
204
+ payload = {
205
+ "ts": datetime.now(timezone.utc).isoformat(),
206
+ "user_name": session["name"],
207
+ "user_email": session["email"],
208
+ "triplet_id": tid,
209
+ "left_method": session["left_method"],
210
+ "right_method": session["right_method"],
211
+ "chosen_side": chosen_side,
212
+ "chosen_method": chosen_method,
213
+ "session_id": session["session_id"],
214
+ }
215
+ path = f"judgments/{email_slug(session['email'])}__{tid}__{ts}.json"
216
+ api.upload_file(
217
+ path_or_fileobj=io.BytesIO(json.dumps(payload, indent=2).encode()),
218
+ path_in_repo=path,
219
+ repo_id=RESULTS_REPO,
220
+ repo_type="dataset",
221
+ commit_message=f"judgment {tid} by {session['email']}",
222
+ )
223
+
224
+
225
+ def choose(side: str, session: dict):
226
+ if not session.get("queue") or session["idx"] >= len(session["queue"]):
227
+ return (
228
+ gr.update(), gr.update(), gr.update(),
229
+ gr.update(),
230
+ session,
231
+ gr.update(interactive=False), gr.update(interactive=False),
232
+ gr.update(value="No more triplets.", visible=True),
233
+ )
234
+ try:
235
+ _write_judgment(session, side)
236
+ except Exception as exc:
237
+ return (
238
+ gr.update(), gr.update(), gr.update(),
239
+ gr.update(value=_progress(session)),
240
+ session,
241
+ gr.update(interactive=True), gr.update(interactive=True),
242
+ gr.update(value=f"Could not save judgment: {exc!r}", visible=True),
243
+ )
244
+
245
+ session["idx"] += 1
246
+ session["n_done_now"] += 1
247
+
248
+ if session["idx"] >= len(session["queue"]):
249
+ return (
250
+ gr.update(value=None), gr.update(value=None), gr.update(value=None),
251
+ gr.update(value=f"All {session['n_total']} triplets done — thank you!"),
252
+ session,
253
+ gr.update(interactive=False), gr.update(interactive=False),
254
+ gr.update(
255
+ value=f"All done! You judged {session['n_done_now']} triplets this session.",
256
+ visible=True,
257
+ ),
258
+ )
259
+
260
+ _assign_sides(session)
261
+ raw, left, right = _paths_for_current(session)
262
+ return (
263
+ gr.update(value=raw), gr.update(value=left), gr.update(value=right),
264
+ gr.update(value=_progress(session)),
265
+ session,
266
+ gr.update(interactive=True), gr.update(interactive=True),
267
+ gr.update(visible=False),
268
+ )
269
+
270
+
271
+ # ---------- UI ----------------------------------------------------------------
272
+
273
+
274
+ with gr.Blocks(title="Denoising A/B Judging", theme=gr.themes.Soft()) as demo:
275
+ session_state = gr.State(_empty_session())
276
+
277
+ gr.Markdown("# Denoising A/B Judging")
278
+ gr.Markdown(
279
+ "For each triplet you'll see a **raw** image and two denoised options "
280
+ "(**A** and **B**). Pick the one you think is the better denoising. "
281
+ "Click any image to zoom in."
282
+ )
283
+
284
+ with gr.Group(visible=True) as login_group:
285
+ gr.Markdown("### Sign in to start")
286
+ name_in = gr.Textbox(label="Name", placeholder="Your name")
287
+ email_in = gr.Textbox(label="Email", placeholder="you@example.com")
288
+ start_btn = gr.Button("Start judging", variant="primary")
289
+ login_error = gr.Markdown(visible=False)
290
+
291
+ with gr.Group(visible=False) as judging_group:
292
+ progress_md = gr.Markdown("")
293
+ with gr.Row():
294
+ raw_img = gr.Image(
295
+ label="Raw",
296
+ type="filepath",
297
+ interactive=False,
298
+ show_download_button=False,
299
+ show_fullscreen_button=True,
300
+ height=420,
301
+ )
302
+ with gr.Row():
303
+ with gr.Column():
304
+ left_img = gr.Image(
305
+ label="Option A",
306
+ type="filepath",
307
+ interactive=False,
308
+ show_download_button=False,
309
+ show_fullscreen_button=True,
310
+ height=420,
311
+ )
312
+ a_btn = gr.Button("A is better", variant="primary")
313
+ with gr.Column():
314
+ right_img = gr.Image(
315
+ label="Option B",
316
+ type="filepath",
317
+ interactive=False,
318
+ show_download_button=False,
319
+ show_fullscreen_button=True,
320
+ height=420,
321
+ )
322
+ b_btn = gr.Button("B is better", variant="primary")
323
+ done_md = gr.Markdown(visible=False)
324
+
325
+ start_outputs = [
326
+ login_group, judging_group, login_error,
327
+ raw_img, left_img, right_img,
328
+ progress_md, session_state,
329
+ a_btn, b_btn, done_md,
330
+ ]
331
+ start_btn.click(start, inputs=[name_in, email_in], outputs=start_outputs)
332
+
333
+ choose_outputs = [
334
+ raw_img, left_img, right_img,
335
+ progress_md, session_state,
336
+ a_btn, b_btn, done_md,
337
+ ]
338
+ a_btn.click(lambda s: choose("A", s), inputs=[session_state], outputs=choose_outputs)
339
+ b_btn.click(lambda s: choose("B", s), inputs=[session_state], outputs=choose_outputs)
340
+
341
+
342
+ if __name__ == "__main__":
343
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio>=5.0
2
+ huggingface_hub>=0.26