lanczos commited on
Commit
000a5ee
·
verified ·
1 Parent(s): f5a1931

deploy: labeling server

Browse files
labeling/static/app.js CHANGED
@@ -161,6 +161,7 @@ async function renderDonePage(token, taskData) {
161
  if (taskData.reason !== "cap_reached") {
162
  // Pool is drained entirely. Thank and stop.
163
  card.innerHTML = `<p class='done'>All items are fully labeled (you contributed ${labeled}). Thank you!</p>`;
 
164
  return;
165
  }
166
 
@@ -188,9 +189,8 @@ async function renderDonePage(token, taskData) {
188
  const btn = document.createElement("button");
189
  btn.id = "retry-session";
190
  if (status.round_number === 1) {
191
- btn.textContent = "Try again (careful labeling)";
192
  btn.addEventListener("click", () => {
193
- // Round-1 fail: wipe everything — no email on file yet.
194
  localStorage.removeItem(TOKEN_STORAGE_KEY);
195
  localStorage.removeItem(EMAIL_STORAGE_KEY);
196
  location.reload();
@@ -198,7 +198,6 @@ async function renderDonePage(token, taskData) {
198
  } else {
199
  btn.textContent = `Redo round ${status.round_number}`;
200
  btn.addEventListener("click", async () => {
201
- // Round 2+ fail: clear token but keep email, re-register same round.
202
  localStorage.removeItem(TOKEN_STORAGE_KEY);
203
  const email = status.email || localStorage.getItem(EMAIL_STORAGE_KEY);
204
  if (!email) { location.reload(); return; }
@@ -215,15 +214,15 @@ async function renderDonePage(token, taskData) {
215
  });
216
  }
217
  card.appendChild(btn);
 
218
  return;
219
  }
220
 
221
  // Pass state
222
  if (status.round_number === 1 && !status.email) {
223
- // Needs email submission
224
  msg.innerHTML =
225
- `<strong>Great job!</strong> Your round-1 session passed the quality check. ` +
226
- `Submit your email to be entered in the lottery.`;
227
  const form = document.createElement("form");
228
  form.id = "email-form";
229
  form.innerHTML =
@@ -235,41 +234,37 @@ async function renderDonePage(token, taskData) {
235
  const email = document.getElementById("email-input").value.trim();
236
  if (!email) return;
237
  try {
238
- const resp = await fetchJSON("/api/submit_email", {
239
  method: "POST",
240
  headers: { "content-type": "application/json" },
241
  body: JSON.stringify({ token, email }),
242
  });
243
  localStorage.setItem(EMAIL_STORAGE_KEY, email);
244
- // Re-render with updated status
245
  await renderDonePage(token, taskData);
246
  } catch (e) {
247
  document.getElementById("error").textContent = `Submit failed: ${e.message}`;
248
  }
249
  });
 
250
  return;
251
  }
252
 
253
- // Already on email chain (round 1 email submitted OR round 2+)
254
- const multiplier = status.multiplier || 1.0;
255
  msg.innerHTML =
256
- `<strong>Thanks, ${status.email}!</strong><br>` +
257
- `Your lottery multiplier is now <strong>${multiplier.toFixed(2)}×</strong> ` +
258
- `(${labeled}-item round ${status.round_number} passed).`;
259
 
260
  if (status.can_extend) {
261
- const extraLeft = 3 - status.round_number;
262
- const nextMultiplier = Math.min(multiplier + 0.25, 1.5);
263
- const info = document.createElement("p");
264
- info.style.color = "var(--muted)";
265
- info.textContent =
266
- `Label ${EXTRA_ROUND_CAP} more to reach ${nextMultiplier.toFixed(2)}× ` +
267
- `(${extraLeft} bonus round${extraLeft > 1 ? "s" : ""} remaining).`;
268
- card.appendChild(info);
269
 
270
  const btn = document.createElement("button");
271
  btn.id = "extend-session";
272
- btn.textContent = `Label ${EXTRA_ROUND_CAP} more (+0.25×)`;
273
  btn.addEventListener("click", async () => {
274
  try {
275
  await registerWithParams({
@@ -284,11 +279,23 @@ async function renderDonePage(token, taskData) {
284
  });
285
  card.appendChild(btn);
286
  } else {
287
- const info = document.createElement("p");
288
- info.style.color = "var(--muted)";
289
- info.textContent = "Maximum lottery multiplier reached. Thanks for labeling!";
290
- card.appendChild(info);
291
  }
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
  function updateProgress(labeled, cap) {
 
161
  if (taskData.reason !== "cap_reached") {
162
  // Pool is drained entirely. Thank and stop.
163
  card.innerHTML = `<p class='done'>All items are fully labeled (you contributed ${labeled}). Thank you!</p>`;
164
+ appendAnnouncement(card);
165
  return;
166
  }
167
 
 
189
  const btn = document.createElement("button");
190
  btn.id = "retry-session";
191
  if (status.round_number === 1) {
192
+ btn.textContent = "Try again";
193
  btn.addEventListener("click", () => {
 
194
  localStorage.removeItem(TOKEN_STORAGE_KEY);
195
  localStorage.removeItem(EMAIL_STORAGE_KEY);
196
  location.reload();
 
198
  } else {
199
  btn.textContent = `Redo round ${status.round_number}`;
200
  btn.addEventListener("click", async () => {
 
201
  localStorage.removeItem(TOKEN_STORAGE_KEY);
202
  const email = status.email || localStorage.getItem(EMAIL_STORAGE_KEY);
203
  if (!email) { location.reload(); return; }
 
214
  });
215
  }
216
  card.appendChild(btn);
217
+ appendAnnouncement(card);
218
  return;
219
  }
220
 
221
  // Pass state
222
  if (status.round_number === 1 && !status.email) {
 
223
  msg.innerHTML =
224
+ `<strong>Great job!</strong> Your session passed the quality check. ` +
225
+ `Submit your email to enter the lottery.`;
226
  const form = document.createElement("form");
227
  form.id = "email-form";
228
  form.innerHTML =
 
234
  const email = document.getElementById("email-input").value.trim();
235
  if (!email) return;
236
  try {
237
+ await fetchJSON("/api/submit_email", {
238
  method: "POST",
239
  headers: { "content-type": "application/json" },
240
  body: JSON.stringify({ token, email }),
241
  });
242
  localStorage.setItem(EMAIL_STORAGE_KEY, email);
 
243
  await renderDonePage(token, taskData);
244
  } catch (e) {
245
  document.getElementById("error").textContent = `Submit failed: ${e.message}`;
246
  }
247
  });
248
+ appendAnnouncement(card);
249
  return;
250
  }
251
 
252
+ // Already on email chain: thank + offer "Label more" if not maxed
253
+ const entries = status.labels_in_lottery ?? 0;
254
  msg.innerHTML =
255
+ `<strong>Thanks, ${status.email}!</strong> You're entered in the lottery ` +
256
+ `with <strong>${entries}</strong> labels counted so far.`;
 
257
 
258
  if (status.can_extend) {
259
+ const hint = document.createElement("p");
260
+ hint.className = "muted-info";
261
+ hint.textContent =
262
+ `Want better odds? Label ${EXTRA_ROUND_CAP} more to add to your entry count.`;
263
+ card.appendChild(hint);
 
 
 
264
 
265
  const btn = document.createElement("button");
266
  btn.id = "extend-session";
267
+ btn.textContent = `Label ${EXTRA_ROUND_CAP} more`;
268
  btn.addEventListener("click", async () => {
269
  try {
270
  await registerWithParams({
 
279
  });
280
  card.appendChild(btn);
281
  } else {
282
+ const hint = document.createElement("p");
283
+ hint.className = "muted-info";
284
+ hint.textContent = "Thanks for participating!";
285
+ card.appendChild(hint);
286
  }
287
+
288
+ appendAnnouncement(card);
289
+ }
290
+
291
+ function appendAnnouncement(card) {
292
+ const div = document.createElement("div");
293
+ div.className = "announcement";
294
+ div.innerHTML =
295
+ `Winning probability is proportional to labels that pass the quality check. ` +
296
+ `Prize pool: <strong>10 × $20 Amazon gift cards</strong>, drawn after ` +
297
+ `we reach 2,000 labels total.`;
298
+ card.appendChild(div);
299
  }
300
 
301
  function updateProgress(labeled, cap) {
labeling/static/index.html CHANGED
@@ -4,7 +4,7 @@
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
  <title>AestheticMCQ — Annotation</title>
7
- <link rel="stylesheet" href="/style.css?v=9" />
8
  <script>
9
  // Apply saved theme before CSS paints to avoid a flash.
10
  (function () {
@@ -41,6 +41,6 @@
41
  <span id="error"></span>
42
  </footer>
43
  </main>
44
- <script src="/app.js?v=13"></script>
45
  </body>
46
  </html>
 
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
  <title>AestheticMCQ — Annotation</title>
7
+ <link rel="stylesheet" href="/style.css?v=10" />
8
  <script>
9
  // Apply saved theme before CSS paints to avoid a flash.
10
  (function () {
 
41
  <span id="error"></span>
42
  </footer>
43
  </main>
44
+ <script src="/app.js?v=14"></script>
45
  </body>
46
  </html>
labeling/static/style.css CHANGED
@@ -219,3 +219,21 @@ button#submit:disabled {
219
  padding: 8px 16px;
220
  margin: 0;
221
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  padding: 8px 16px;
220
  margin: 0;
221
  }
222
+
223
+ .announcement {
224
+ margin: 28px auto 0;
225
+ padding: 12px 16px;
226
+ max-width: 520px;
227
+ font-size: 0.85rem;
228
+ color: var(--muted);
229
+ text-align: center;
230
+ border-top: 1px dashed var(--border);
231
+ line-height: 1.5;
232
+ }
233
+
234
+ .muted-info {
235
+ text-align: center;
236
+ color: var(--muted);
237
+ font-size: 0.9rem;
238
+ margin: 8px auto;
239
+ }
src/aamcq/annotation/api.py CHANGED
@@ -19,22 +19,16 @@ from aamcq.annotation import db as dbmod
19
  from aamcq.annotation.assignment import bootstrap_annotators
20
 
21
  DEFAULT_ACC_THRESHOLD = 0.40
22
- MAX_LOTTERY_ROUND = 3 # round 1 base, +2 bonus rounds cap multiplier at 1.5x
 
 
 
23
  _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
24
 
25
 
26
  def _is_valid_email(email: str) -> bool:
27
  return bool(email) and len(email) <= 254 and bool(_EMAIL_RE.match(email))
28
 
29
-
30
- def _multiplier(passed_rounds: set[int]) -> float:
31
- """Lottery multiplier = 1.0 + 0.25 * (extra passed rounds beyond round 1),
32
- capped at 1.5 (i.e. rounds 1+2+3 all passed)."""
33
- if 1 not in passed_rounds:
34
- return 0.0
35
- extras = len([r for r in passed_rounds if r > 1])
36
- return min(1.0 + 0.25 * extras, 1.5)
37
-
38
  REPO_ROOT = Path(__file__).resolve().parents[3]
39
  DEFAULT_DB = REPO_ROOT / "data" / "annotations.sqlite"
40
  DEFAULT_IMAGE_DIR = REPO_ROOT / "data" / "images"
@@ -326,19 +320,24 @@ def create_app(
326
  # to compute the current multiplier + whether more rounds are
327
  # available.
328
  email = row["email"]
329
- multiplier = 0.0
330
- passed_rounds: set[int] = set()
331
  if email:
332
- passed_rounds = dbmod.email_passed_rounds(
 
 
333
  conn, email, app.state.acc_threshold
334
  )
335
- # Include THIS session's pass optimistically so the UI shows
336
- # the correct multiplier immediately after a passing round
337
- # (row's cap may differ from the session_accuracy lookup if
338
- # server cap default applied; use effective cap instead).
339
  if acc_pass:
340
- passed_rounds = passed_rounds | {int(row["round_number"])}
341
- multiplier = _multiplier(passed_rounds)
 
 
 
 
 
342
 
343
  can_extend = (
344
  acc_pass
@@ -353,9 +352,9 @@ def create_app(
353
  "acc_pass": bool(acc_pass),
354
  "round_number": int(row["round_number"]),
355
  "email": email,
356
- "multiplier": multiplier,
357
  "can_extend": bool(can_extend),
358
- "next_round_cap": 10, # hardcoded for now; frontend uses this
359
  }
360
 
361
  @app.post("/api/submit_email")
@@ -411,14 +410,11 @@ def create_app(
411
  )
412
 
413
  dbmod.set_annotator_email(conn, annotator_id, payload.email)
414
- passed_rounds = dbmod.email_passed_rounds(
415
- conn, payload.email, app.state.acc_threshold
416
- )
417
  return {
418
  "ok": True,
419
  "email": payload.email,
420
  "round_number": 1,
421
- "multiplier": _multiplier(passed_rounds),
422
  }
423
 
424
  @app.get("/api/progress")
 
19
  from aamcq.annotation.assignment import bootstrap_annotators
20
 
21
  DEFAULT_ACC_THRESHOLD = 0.40
22
+ # Round 1 (cap=20) + up to 3 bonus rounds (cap=10 each) = 50 labels max per
23
+ # annotator. Each label that lives in a passing session counts as one
24
+ # lottery entry; more labels = better odds.
25
+ MAX_LOTTERY_ROUND = 4
26
  _EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
27
 
28
 
29
  def _is_valid_email(email: str) -> bool:
30
  return bool(email) and len(email) <= 254 and bool(_EMAIL_RE.match(email))
31
 
 
 
 
 
 
 
 
 
 
32
  REPO_ROOT = Path(__file__).resolve().parents[3]
33
  DEFAULT_DB = REPO_ROOT / "data" / "annotations.sqlite"
34
  DEFAULT_IMAGE_DIR = REPO_ROOT / "data" / "images"
 
320
  # to compute the current multiplier + whether more rounds are
321
  # available.
322
  email = row["email"]
323
+ labels_in_lottery = 0
 
324
  if email:
325
+ # Count labels across ALL this email's passing annotators;
326
+ # each label is one lottery entry.
327
+ labels_in_lottery = dbmod.email_passed_label_count(
328
  conn, email, app.state.acc_threshold
329
  )
330
+ # Include THIS session optimistically if it just passed — the
331
+ # helper's scan excludes uncommitted state, and we want the
332
+ # UI to reflect the new total immediately.
 
333
  if acc_pass:
334
+ # Avoid double-counting if this annotator's already
335
+ # persisted labels would have been counted.
336
+ passed_rounds = dbmod.email_passed_rounds(
337
+ conn, email, app.state.acc_threshold
338
+ )
339
+ if int(row["round_number"]) not in passed_rounds:
340
+ labels_in_lottery += int(n)
341
 
342
  can_extend = (
343
  acc_pass
 
352
  "acc_pass": bool(acc_pass),
353
  "round_number": int(row["round_number"]),
354
  "email": email,
355
+ "labels_in_lottery": int(labels_in_lottery),
356
  "can_extend": bool(can_extend),
357
+ "next_round_cap": 10,
358
  }
359
 
360
  @app.post("/api/submit_email")
 
410
  )
411
 
412
  dbmod.set_annotator_email(conn, annotator_id, payload.email)
 
 
 
413
  return {
414
  "ok": True,
415
  "email": payload.email,
416
  "round_number": 1,
417
+ "labels_in_lottery": int(n),
418
  }
419
 
420
  @app.get("/api/progress")
src/aamcq/annotation/db.py CHANGED
@@ -311,6 +311,26 @@ def email_passed_rounds(
311
  return passed
312
 
313
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  def count_annotator_labels(conn: sqlite3.Connection, annotator_id: str) -> int:
315
  return int(conn.execute(
316
  "SELECT COUNT(*) AS n FROM labels WHERE annotator_id = ?",
 
311
  return passed
312
 
313
 
314
+ def email_passed_label_count(
315
+ conn: sqlite3.Connection, email: str, acc_threshold: float
316
+ ) -> int:
317
+ """Total labels across all of this email's annotators whose session
318
+ (a) hit their cap and (b) has acc >= threshold. Each label = 1
319
+ lottery entry."""
320
+ rows = conn.execute(
321
+ "SELECT annotator_id, cap FROM annotators WHERE email = ?", (email,)
322
+ ).fetchall()
323
+ total = 0
324
+ for r in rows:
325
+ n_correct, n = session_accuracy(conn, r["annotator_id"])
326
+ cap = r["cap"]
327
+ if cap is None or n < cap or n == 0:
328
+ continue
329
+ if (n_correct / n) >= acc_threshold:
330
+ total += n
331
+ return total
332
+
333
+
334
  def count_annotator_labels(conn: sqlite3.Connection, annotator_id: str) -> int:
335
  return int(conn.execute(
336
  "SELECT COUNT(*) AS n FROM labels WHERE annotator_id = ?",