NidhiS09 commited on
Commit
b39787e
Β·
verified Β·
1 Parent(s): ac34ccc

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +237 -238
main.py CHANGED
@@ -3,8 +3,10 @@ import json
3
  import uuid
4
  import time
5
  import tempfile
 
 
6
  from datetime import datetime, timezone
7
- from typing import Any, Dict, List, Tuple
8
 
9
  import streamlit as st
10
  import pandas as pd
@@ -36,83 +38,213 @@ PHASES = [
36
  ]
37
 
38
  CHALLENGE_TYPES = ["Object Detection", "Instance Segmentation"]
39
-
40
  LEADERBOARD_METRICS = ["bbox_mAP", "bbox_AP50", "segm_mAP", "segm_AP50"]
41
  DEFAULT_SORT_METRIC = "segm_AP50"
 
42
 
43
  # =========================
44
- # AUTH HELPERS
 
 
 
 
 
 
 
 
 
 
45
  # =========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
- def get_oauth_info() -> Dict | None:
48
- """
49
- In Docker spaces, HF injects user info via the X-Space-User-Info request header
50
- as a JSON string once the user has authenticated via /oauth/login.
51
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  try:
53
- headers = st.context.headers
54
- user_info_raw = headers.get("X-Space-User-Info")
55
- if user_info_raw:
56
- data = json.loads(user_info_raw)
57
- return {
58
- "preferred_username": data.get("preferred_username", data.get("name", "")),
59
- "name": data.get("name", ""),
60
- "email": data.get("email", ""),
61
- "picture": data.get("picture", None),
62
- }
63
  except Exception:
64
  pass
65
- return None
66
 
 
 
 
67
 
68
- def get_login_url() -> str:
69
- """
70
- In HF Docker spaces, OAuth login is at /oauth/login on the space host.
71
- HF injects SPACE_HOST as an env var automatically.
72
- """
73
- space_host = os.getenv("SPACE_HOST", "")
74
- if space_host:
75
- return f"https://{space_host}/oauth/login"
76
- return "/oauth/login"
77
 
 
 
78
 
79
- def render_login_wall():
80
- """Shows login prompt and stops rendering if not authenticated."""
81
- user = get_oauth_info()
 
 
 
82
  if user:
83
  return user
84
-
85
- st.markdown("---")
86
- st.markdown("## πŸ” Login Required")
87
- st.info(
88
- "You need to log in with your Hugging Face account to submit predictions "
89
- "or view your submission history."
90
- )
91
- login_url = get_login_url()
92
- st.markdown(
93
- f'''<a href="{login_url}" target="_self">
94
- <button style="background:#FF6B35;color:white;border:none;padding:12px 28px;
95
- font-size:16px;border-radius:8px;cursor:pointer;font-weight:bold;">
96
- πŸ€— Login with Hugging Face</button></a>''',
97
- unsafe_allow_html=True,
98
- )
99
  st.stop()
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
 
102
  # =========================
103
- # SUBMISSION CAP HELPERS
104
  # =========================
105
 
 
 
 
 
 
106
  def _today_utc_str() -> str:
107
  return datetime.now(timezone.utc).strftime("%Y-%m-%d")
108
 
109
-
110
- def _count_submissions_today(hf_username: str) -> int:
111
- """
112
- Count how many submissions this user has made today by scanning submission metadata.
113
- We do this by listing all submission files and checking meta.json for matching username + today's date.
114
- Uses a lightweight approach: filter by filename listing only.
115
- """
116
  try:
117
  files = api_client().list_repo_files(
118
  repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE, token=SUBMISSIONS_TOKEN
@@ -124,13 +256,11 @@ def _count_submissions_today(hf_username: str) -> int:
124
  continue
125
  try:
126
  p = hf_hub_download(
127
- repo_id=DB_REPO_ID,
128
- repo_type=DB_REPO_TYPE,
129
- filename=f,
130
- token=SUBMISSIONS_TOKEN,
131
  )
132
  meta = json.load(open(p))
133
- if meta.get("hf_username") == hf_username:
134
  ts = meta.get("timestamp", 0)
135
  sub_date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
136
  if sub_date == today:
@@ -141,30 +271,10 @@ def _count_submissions_today(hf_username: str) -> int:
141
  except Exception:
142
  return 0
143
 
144
-
145
- # =========================
146
- # HF API HELPERS
147
- # =========================
148
-
149
- @st.cache_resource
150
- def api_client() -> HfApi:
151
- return HfApi()
152
-
153
-
154
- def _require_token() -> None:
155
- if not SUBMISSIONS_TOKEN:
156
- st.error(
157
- "Missing SUBMISSIONS_TOKEN. Add it in Space Settings β†’ Secrets."
158
- )
159
- st.stop()
160
-
161
-
162
  def _validate_submission_json(obj: Any) -> Tuple[bool, str]:
163
  if not isinstance(obj, list):
164
  return False, "Submission must be a JSON list of annotations."
165
-
166
  required_keys = {"image_id", "score", "category_id", "area", "bbox", "segmentation"}
167
-
168
  for i, ann in enumerate(obj):
169
  if not isinstance(ann, dict):
170
  return False, f"Annotation at index {i} must be an object/dict."
@@ -181,13 +291,11 @@ def _validate_submission_json(obj: Any) -> Tuple[bool, str]:
181
  return False, f"area at index {i} must be a number."
182
  bbox = ann["bbox"]
183
  if not (isinstance(bbox, list) and len(bbox) == 4 and all(isinstance(x, (int, float)) for x in bbox)):
184
- return False, f"bbox at index {i} must be a list of 4 numbers: [x, y, w, h]."
185
  if not isinstance(ann["segmentation"], list):
186
  return False, f"segmentation at index {i} must be a list."
187
-
188
  return True, "OK"
189
 
190
-
191
  def _upload_json(data: Any, path_in_repo: str, commit_message: str = "") -> None:
192
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
193
  json.dump(data, tmp, ensure_ascii=False)
@@ -207,23 +315,11 @@ def _upload_json(data: Any, path_in_repo: str, commit_message: str = "") -> None
207
  except OSError:
208
  pass
209
 
210
-
211
- def _create_submission_record(
212
- *,
213
- pred: List[Dict[str, Any]],
214
- team: str,
215
- model_name: str,
216
- phase_codename: str,
217
- challenge_type: str,
218
- original_filename: str,
219
- hf_username: str,
220
- hf_email: str,
221
- ) -> str:
222
  _require_token()
223
-
224
  submission_id = str(uuid.uuid4())
225
  ts = int(time.time())
226
-
227
  meta = {
228
  "submission_id": submission_id,
229
  "team": team.strip(),
@@ -232,28 +328,22 @@ def _create_submission_record(
232
  "challenge_type": challenge_type,
233
  "timestamp": ts,
234
  "original_filename": original_filename,
235
- "hf_username": hf_username,
236
- "hf_email": hf_email,
237
  }
238
-
239
  status = {"state": "queued", "timestamp": ts}
240
  base = f"submissions/{submission_id}"
241
-
242
  _upload_json(pred, f"{base}/pred.json", f"pred {submission_id}")
243
  _upload_json(meta, f"{base}/meta.json", f"meta {submission_id}")
244
  _upload_json(status, f"{base}/status.json", f"status {submission_id}")
245
-
246
  return submission_id
247
 
248
-
249
  def _load_leaderboard_df() -> pd.DataFrame:
250
  _require_token()
251
  try:
252
  path = hf_hub_download(
253
- repo_id=DB_REPO_ID,
254
- repo_type=DB_REPO_TYPE,
255
- filename="leaderboard.jsonl",
256
- token=SUBMISSIONS_TOKEN,
257
  )
258
  except HfHubHTTPError as e:
259
  if "404" in str(e):
@@ -278,15 +368,11 @@ def _load_leaderboard_df() -> pd.DataFrame:
278
  for col in ["team", "model", "phase_codename", "timestamp", *LEADERBOARD_METRICS]:
279
  if col not in df.columns:
280
  df[col] = None
281
-
282
  if DEFAULT_SORT_METRIC in df.columns:
283
  df = df.sort_values(by=DEFAULT_SORT_METRIC, ascending=False, kind="mergesort")
284
-
285
  return df
286
 
287
-
288
- def _load_user_submissions(hf_username: str) -> List[Dict]:
289
- """Load all submissions for a specific HF user."""
290
  _require_token()
291
  try:
292
  files = api_client().list_repo_files(
@@ -306,11 +392,8 @@ def _load_user_submissions(hf_username: str) -> List[Dict]:
306
  filename=f, token=SUBMISSIONS_TOKEN
307
  )
308
  meta = json.load(open(meta_path))
309
-
310
- if meta.get("hf_username") != hf_username:
311
  continue
312
-
313
- # Get status
314
  try:
315
  status_path = hf_hub_download(
316
  repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
@@ -319,12 +402,7 @@ def _load_user_submissions(hf_username: str) -> List[Dict]:
319
  status = json.load(open(status_path))
320
  except Exception:
321
  status = {"state": "unknown"}
322
-
323
- # Get results if done
324
- metrics = {}
325
- if status.get("state") == "done":
326
- metrics = status.get("metrics", {})
327
-
328
  results.append({
329
  "submission_id": sid,
330
  "team": meta.get("team", ""),
@@ -341,7 +419,6 @@ def _load_user_submissions(hf_username: str) -> List[Dict]:
341
  results.sort(key=lambda x: x["timestamp"], reverse=True)
342
  return results
343
 
344
-
345
  # =========================
346
  # UI PAGES
347
  # =========================
@@ -350,7 +427,6 @@ def render_overview():
350
  with st.expander("ℹ️ Overview of the VizWiz Benchmark Arena"):
351
  st.markdown("""
352
  **VizWiz Object Localization Challenge** β€” Submit your model predictions and get scored automatically.
353
-
354
  - πŸ“€ **Submit**: Upload your prediction JSON for Dev, Standard, or Challenge phases
355
  - πŸ† **Leaderboard**: View public rankings for the Standard phase
356
  - πŸ“‹ **My Submissions**: Track all your past submissions and scores
@@ -361,19 +437,16 @@ def render_overview():
361
  except Exception:
362
  pass
363
 
364
-
365
  def render_eval_details():
366
  with st.expander("πŸ“ How is the Score Calculated?"):
367
  st.markdown("""
368
  Your submission is evaluated automatically against hidden ground-truth annotations.
369
- The leaderboard reports:
370
  - **bbox_mAP** β€” bounding box mean average precision
371
  - **bbox_AP50** β€” bounding box AP at IoU=0.50
372
- - **segm_mAP** β€” segmentation mean average precision
373
  - **segm_AP50** β€” segmentation AP at IoU=0.50 *(default ranking)*
374
  """)
375
 
376
-
377
  def page_leaderboard():
378
  st.header("πŸ† Leaderboard β€” Standard Phase")
379
  st.caption(f"Ranked by **{DEFAULT_SORT_METRIC}** (descending). Showing Standard phase submissions only.")
@@ -385,7 +458,6 @@ def page_leaderboard():
385
  st.error(f"Could not load leaderboard: {e}")
386
  return
387
 
388
- # Filter to standard phase only for public leaderboard
389
  if not df.empty and "phase_codename" in df.columns:
390
  df = df[df["phase_codename"] == "test-standard2024"]
391
 
@@ -397,9 +469,7 @@ def page_leaderboard():
397
  df_display.insert(0, "Rank", range(1, len(df_display) + 1))
398
  if "timestamp" in df_display.columns:
399
  df_display["timestamp"] = pd.to_datetime(df_display["timestamp"], unit="s", errors="coerce")
400
-
401
- # Drop hf_username / hf_email from public view
402
- for col in ["hf_username", "hf_email", "phase_codename", "submission_id"]:
403
  if col in df_display.columns:
404
  df_display.drop(columns=[col], inplace=True)
405
 
@@ -407,43 +477,28 @@ def page_leaderboard():
407
  df_display,
408
  column_config={
409
  "Rank": st.column_config.Column("Rank", width="small"),
410
- "team": "Team",
411
- "model": "Model",
412
  "bbox_mAP": st.column_config.NumberColumn("bbox_mAP", format="%.4f"),
413
  "bbox_AP50": st.column_config.NumberColumn("bbox_AP50", format="%.4f"),
414
  "segm_mAP": st.column_config.NumberColumn("segm_mAP", format="%.4f"),
415
  "segm_AP50": st.column_config.NumberColumn("segm_AP50", format="%.4f"),
416
  "timestamp": st.column_config.DatetimeColumn("Scored at", format="D MMM YYYY, h:mm a"),
417
  },
418
- use_container_width=True,
419
- hide_index=True,
420
  )
421
 
422
-
423
  def page_submit():
424
  user = render_login_wall()
425
- hf_username = user.get("preferred_username", "unknown")
426
- hf_email = user.get("email", "")
427
 
428
  st.header("πŸš€ Submit Your Predictions")
429
 
430
- # Show user info + daily cap status
431
  col_info, col_cap = st.columns([3, 1])
432
  with col_info:
433
- avatar = user.get("picture", "")
434
- name = user.get("name", hf_username)
435
- if avatar:
436
- st.markdown(
437
- f'<img src="{avatar}" width="32" style="border-radius:50%;vertical-align:middle;margin-right:8px">'
438
- f'<b>{name}</b> (@{hf_username})',
439
- unsafe_allow_html=True,
440
- )
441
- else:
442
- st.markdown(f"πŸ‘€ Logged in as **{name}** (@{hf_username})")
443
-
444
  with col_cap:
445
  with st.spinner("Checking daily limit..."):
446
- subs_today = _count_submissions_today(hf_username)
447
  remaining = DAILY_SUBMISSION_CAP - subs_today
448
  color = "green" if remaining > 1 else ("orange" if remaining == 1 else "red")
449
  st.markdown(
@@ -454,141 +509,104 @@ def page_submit():
454
  )
455
 
456
  st.markdown("---")
457
-
458
  if remaining <= 0:
459
  st.error(f"β›” You've reached your daily limit of {DAILY_SUBMISSION_CAP} submissions. Come back tomorrow!")
460
  return
461
 
462
  col1, col2 = st.columns([2, 1])
463
-
464
  with col2:
465
  st.subheader("Submission Info")
466
- team = st.text_input("Team / Display Name", value=st.session_state.get("team", ""))
467
  model_name = st.text_input("Model Name", value=st.session_state.get("model_name", ""))
468
  phase_label = st.selectbox("Phase", [p["label"] for p in PHASES])
469
  phase_codename = next(p["codename"] for p in PHASES if p["label"] == phase_label)
470
  challenge_type = st.radio("Challenge type", CHALLENGE_TYPES, horizontal=False)
471
  st.session_state["team"] = team
472
  st.session_state["model_name"] = model_name
473
- st.caption("Submissions are scored automatically. Results appear in My Submissions once processed.")
474
 
475
  with col1:
476
  st.subheader("Upload Submission File")
477
  uploaded_file = st.file_uploader("Choose a JSON file", type=["json"])
478
-
479
  if uploaded_file is None:
480
  st.info("Upload a JSON file containing a list of annotations.")
481
  return
482
-
483
  try:
484
  raw = uploaded_file.getvalue().decode("utf-8")
485
  pred_obj = json.loads(raw)
486
  except Exception:
487
- st.error("Could not parse JSON. Please upload a valid JSON file.")
488
  return
489
-
490
  ok, msg = _validate_submission_json(pred_obj)
491
  if not ok:
492
  st.error(f"Invalid submission format: {msg}")
493
  return
494
-
495
  st.success("Submission file looks valid βœ…")
496
 
497
- submit_clicked = st.button("Submit (Queue for Evaluation)", type="primary")
498
-
499
- if submit_clicked:
500
  if not team.strip():
501
  st.error("Please enter a Team / Display Name.")
502
  return
503
  if not model_name.strip():
504
  st.error("Please enter a Model Name.")
505
  return
506
-
507
- # Re-check cap right before submitting
508
- subs_today_fresh = _count_submissions_today(hf_username)
509
- if subs_today_fresh >= DAILY_SUBMISSION_CAP:
510
- st.error("β›” Daily submission limit reached. Try again tomorrow.")
511
  return
512
-
513
  with st.spinner("Uploading submission..."):
514
  try:
515
  submission_id = _create_submission_record(
516
- pred=pred_obj,
517
- team=team,
518
- model_name=model_name,
519
- phase_codename=phase_codename,
520
- challenge_type=challenge_type,
521
  original_filename=uploaded_file.name,
522
- hf_username=hf_username,
523
- hf_email=hf_email,
524
  )
525
  except Exception as e:
526
  st.error(f"Upload failed: {e}")
527
  return
528
-
529
  st.balloons()
530
  st.success("Submission queued successfully!")
531
  st.code(f"Submission ID: {submission_id}")
532
- st.info("Track your results in **My Submissions** once the evaluator processes your file.")
533
-
534
 
535
  def page_my_submissions():
536
  user = render_login_wall()
537
- hf_username = user.get("preferred_username", "unknown")
538
 
539
  st.header("πŸ“‹ My Submissions")
540
- st.caption(f"Showing all submissions for @{hf_username}")
541
 
542
  with st.spinner("Loading your submissions..."):
543
- submissions = _load_user_submissions(hf_username)
544
 
545
  if not submissions:
546
- st.info("You haven't made any submissions yet. Head to **Submit Model** to get started!")
547
  return
548
 
549
- # Phase filter
550
  phases_present = sorted(set(s["phase"] for s in submissions))
551
  selected_phase = st.selectbox("Filter by phase", ["All"] + phases_present)
552
-
553
  filtered = submissions if selected_phase == "All" else [s for s in submissions if s["phase"] == selected_phase]
554
 
555
- # State badges
556
- state_colors = {
557
- "queued": "🟑",
558
- "running": "πŸ”΅",
559
- "done": "🟒",
560
- "failed": "πŸ”΄",
561
- "unknown": "βšͺ",
562
- }
563
-
564
  df = pd.DataFrame(filtered)
565
-
566
  if "timestamp" in df.columns:
567
  df["submitted_at"] = pd.to_datetime(df["timestamp"], unit="s", errors="coerce")
568
-
569
  if "state" in df.columns:
570
  df["status"] = df["state"].apply(lambda s: f"{state_colors.get(s, 'βšͺ')} {s.capitalize()}")
571
 
572
  display_cols = ["submitted_at", "status", "team", "model", "phase", "challenge_type"]
573
  metric_cols = [m for m in LEADERBOARD_METRICS if m in df.columns]
574
  display_cols += metric_cols
575
-
576
  df_display = df[[c for c in display_cols if c in df.columns]].copy()
577
 
578
  col_config = {
579
  "submitted_at": st.column_config.DatetimeColumn("Submitted At", format="D MMM YYYY, h:mm a"),
580
- "status": "Status",
581
- "team": "Team",
582
- "model": "Model",
583
- "phase": "Phase",
584
- "challenge_type": "Challenge Type",
585
  }
586
  for m in metric_cols:
587
  col_config[m] = st.column_config.NumberColumn(m, format="%.4f")
588
 
589
  st.dataframe(df_display, column_config=col_config, use_container_width=True, hide_index=True)
590
 
591
- # Summary stats
592
  st.markdown("---")
593
  total = len(submissions)
594
  done = sum(1 for s in submissions if s["state"] == "done")
@@ -596,56 +614,29 @@ def page_my_submissions():
596
  1 for s in submissions
597
  if datetime.fromtimestamp(s["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d") == _today_utc_str()
598
  )
599
-
600
  c1, c2, c3 = st.columns(3)
601
  c1.metric("Total Submissions", total)
602
  c2.metric("Scored", done)
603
  c3.metric(f"Today ({DAILY_SUBMISSION_CAP} max)", f"{today_count}/{DAILY_SUBMISSION_CAP}")
604
 
605
-
606
  # =========================
607
  # MAIN
608
  # =========================
609
 
610
  def main():
611
  st.sidebar.title("VizWiz Benchmark πŸ†")
612
-
613
- # Check login state for sidebar display
614
- user = get_oauth_info()
615
 
616
  if user:
617
- hf_username = user.get("preferred_username", "")
618
- avatar = user.get("picture", "")
619
- if avatar:
620
- st.sidebar.markdown(
621
- f'<img src="{avatar}" width="40" style="border-radius:50%"><br>'
622
- f'<b>@{hf_username}</b>',
623
- unsafe_allow_html=True,
624
- )
625
- else:
626
- st.sidebar.markdown(f"πŸ‘€ @{hf_username}")
627
- space_host = os.getenv("SPACE_HOST", "")
628
- logout_url = f"https://{space_host}/oauth/logout" if space_host else "/oauth/logout"
629
- st.sidebar.markdown(
630
- f'''<a href="{logout_url}" target="_self">
631
- <button style="width:100%;background:#555;color:white;border:none;
632
- padding:8px;border-radius:6px;cursor:pointer;font-size:13px;">
633
- Logout</button></a>''',
634
- unsafe_allow_html=True,
635
- )
636
  st.sidebar.markdown("---")
637
  menu = ["Leaderboard", "Submit Model", "My Submissions"]
638
  else:
639
- st.sidebar.info("Login to submit predictions and view your history.")
640
- login_url = get_login_url()
641
- st.sidebar.markdown(
642
- f'''<a href="{login_url}" target="_self">
643
- <button style="width:100%;background:#FF6B35;color:white;border:none;
644
- padding:10px;border-radius:6px;cursor:pointer;font-size:14px;font-weight:bold;">
645
- πŸ€— Login with HF</button></a>''',
646
- unsafe_allow_html=True,
647
- )
648
- menu = ["Leaderboard", "Submit Model", "My Submissions"]
649
 
650
  choice = st.sidebar.radio("Navigation", menu)
651
  st.sidebar.markdown("---")
@@ -657,11 +648,19 @@ def main():
657
 
658
  if choice == "Leaderboard":
659
  page_leaderboard()
 
 
 
 
 
 
 
 
 
660
  elif choice == "Submit Model":
661
  page_submit()
662
  elif choice == "My Submissions":
663
  page_my_submissions()
664
 
665
-
666
  if __name__ == "__main__":
667
  main()
 
3
  import uuid
4
  import time
5
  import tempfile
6
+ import hashlib
7
+ import hmac
8
  from datetime import datetime, timezone
9
+ from typing import Any, Dict, List, Tuple, Optional
10
 
11
  import streamlit as st
12
  import pandas as pd
 
38
  ]
39
 
40
  CHALLENGE_TYPES = ["Object Detection", "Instance Segmentation"]
 
41
  LEADERBOARD_METRICS = ["bbox_mAP", "bbox_AP50", "segm_mAP", "segm_AP50"]
42
  DEFAULT_SORT_METRIC = "segm_AP50"
43
+ USERS_FILE = "users.jsonl"
44
 
45
  # =========================
46
+ # PASSWORD HELPERS
47
+ # =========================
48
+
49
+ def _hash_password(password: str) -> str:
50
+ """SHA-256 hash with a salt stored in env, no bcrypt dependency needed."""
51
+ salt = os.getenv("PASSWORD_SALT", "vizwiz-benchmark-salt-2024")
52
+ return hashlib.sha256(f"{salt}{password}".encode()).hexdigest()
53
+
54
+ def _verify_password(password: str, hashed: str) -> bool:
55
+ return hmac.compare_digest(_hash_password(password), hashed)
56
+
57
  # =========================
58
+ # USER STORE (users.jsonl in private dataset)
59
+ # =========================
60
+
61
+ @st.cache_resource
62
+ def api_client() -> HfApi:
63
+ return HfApi()
64
+
65
+ def _load_users() -> Dict[str, Dict]:
66
+ """Returns dict of username -> user record."""
67
+ try:
68
+ path = hf_hub_download(
69
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
70
+ filename=USERS_FILE, token=SUBMISSIONS_TOKEN
71
+ )
72
+ users = {}
73
+ with open(path, "r") as f:
74
+ for line in f:
75
+ line = line.strip()
76
+ if line:
77
+ try:
78
+ u = json.loads(line)
79
+ users[u["username"].lower()] = u
80
+ except Exception:
81
+ continue
82
+ return users
83
+ except HfHubHTTPError as e:
84
+ if "404" in str(e):
85
+ return {}
86
+ raise
87
+ except Exception:
88
+ return {}
89
 
90
+ def _save_user(user: Dict) -> None:
91
+ """Append a new user to users.jsonl."""
92
+ try:
93
+ path = hf_hub_download(
94
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
95
+ filename=USERS_FILE, token=SUBMISSIONS_TOKEN
96
+ )
97
+ existing = open(path, "r").read()
98
+ except HfHubHTTPError as e:
99
+ existing = "" if "404" in str(e) else (_ for _ in ()).throw(e)
100
+
101
+ updated = (existing.rstrip("\n") + "\n" + json.dumps(user) + "\n") if existing.strip() else (json.dumps(user) + "\n")
102
+
103
+ with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as f:
104
+ f.write(updated)
105
+ tmp = f.name
106
+
107
+ api_client().upload_file(
108
+ path_or_fileobj=tmp,
109
+ path_in_repo=USERS_FILE,
110
+ repo_id=DB_REPO_ID,
111
+ repo_type=DB_REPO_TYPE,
112
+ token=SUBMISSIONS_TOKEN,
113
+ commit_message=f"new user: {user['username']}",
114
+ )
115
  try:
116
+ os.remove(tmp)
 
 
 
 
 
 
 
 
 
117
  except Exception:
118
  pass
 
119
 
120
+ # =========================
121
+ # AUTH STATE
122
+ # =========================
123
 
124
+ def get_logged_in_user() -> Optional[Dict]:
125
+ return st.session_state.get("user", None)
 
 
 
 
 
 
 
126
 
127
+ def login_user(user: Dict) -> None:
128
+ st.session_state["user"] = user
129
 
130
+ def logout_user() -> None:
131
+ st.session_state.pop("user", None)
132
+
133
+ def render_login_wall() -> Dict:
134
+ """Redirect to login if not authenticated."""
135
+ user = get_logged_in_user()
136
  if user:
137
  return user
138
+ st.warning("Please log in to access this page.")
139
+ st.session_state["redirect_after_login"] = True
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  st.stop()
141
 
142
+ # =========================
143
+ # AUTH PAGES
144
+ # =========================
145
+
146
+ def page_login():
147
+ st.header("πŸ” Login")
148
+ col, _ = st.columns([1, 1])
149
+ with col:
150
+ username = st.text_input("Username", key="login_username").strip().lower()
151
+ password = st.text_input("Password", type="password", key="login_password")
152
+
153
+ if st.button("Login", type="primary", use_container_width=True):
154
+ if not username or not password:
155
+ st.error("Please enter username and password.")
156
+ return
157
+
158
+ with st.spinner("Verifying..."):
159
+ users = _load_users()
160
+
161
+ if username not in users:
162
+ st.error("Username not found.")
163
+ return
164
+
165
+ user = users[username]
166
+ if not _verify_password(password, user["password_hash"]):
167
+ st.error("Incorrect password.")
168
+ return
169
+
170
+ login_user(user)
171
+ st.success(f"Welcome back, {user['username']}!")
172
+ st.rerun()
173
+
174
+ st.markdown("---")
175
+ st.caption("Don't have an account?")
176
+ if st.button("Create an account β†’", use_container_width=True):
177
+ st.session_state["auth_page"] = "signup"
178
+ st.rerun()
179
+
180
+
181
+ def page_signup():
182
+ st.header("✏️ Create Account")
183
+ col, _ = st.columns([1, 1])
184
+ with col:
185
+ username = st.text_input("Username", key="signup_username").strip()
186
+ email = st.text_input("Email", key="signup_email").strip()
187
+ team = st.text_input("Team / Organization", key="signup_team").strip()
188
+ password = st.text_input("Password", type="password", key="signup_password")
189
+ password2 = st.text_input("Confirm Password", type="password", key="signup_password2")
190
+
191
+ if st.button("Create Account", type="primary", use_container_width=True):
192
+ if not all([username, email, team, password, password2]):
193
+ st.error("Please fill in all fields.")
194
+ return
195
+ if len(username) < 3:
196
+ st.error("Username must be at least 3 characters.")
197
+ return
198
+ if password != password2:
199
+ st.error("Passwords do not match.")
200
+ return
201
+ if len(password) < 6:
202
+ st.error("Password must be at least 6 characters.")
203
+ return
204
+
205
+ with st.spinner("Creating account..."):
206
+ users = _load_users()
207
+ if username.lower() in users:
208
+ st.error("Username already taken.")
209
+ return
210
+
211
+ new_user = {
212
+ "username": username,
213
+ "email": email,
214
+ "team": team,
215
+ "password_hash": _hash_password(password),
216
+ "created_at": int(time.time()),
217
+ }
218
+ try:
219
+ _save_user(new_user)
220
+ except Exception as e:
221
+ st.error(f"Failed to create account: {e}")
222
+ return
223
+
224
+ login_user(new_user)
225
+ st.success(f"Account created! Welcome, {username}!")
226
+ st.rerun()
227
+
228
+ st.markdown("---")
229
+ st.caption("Already have an account?")
230
+ if st.button("← Back to Login", use_container_width=True):
231
+ st.session_state["auth_page"] = "login"
232
+ st.rerun()
233
+
234
 
235
  # =========================
236
+ # SUBMISSION HELPERS
237
  # =========================
238
 
239
+ def _require_token() -> None:
240
+ if not SUBMISSIONS_TOKEN:
241
+ st.error("Missing SUBMISSIONS_TOKEN. Add it in Space Settings β†’ Secrets.")
242
+ st.stop()
243
+
244
  def _today_utc_str() -> str:
245
  return datetime.now(timezone.utc).strftime("%Y-%m-%d")
246
 
247
+ def _count_submissions_today(username: str) -> int:
 
 
 
 
 
 
248
  try:
249
  files = api_client().list_repo_files(
250
  repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE, token=SUBMISSIONS_TOKEN
 
256
  continue
257
  try:
258
  p = hf_hub_download(
259
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
260
+ filename=f, token=SUBMISSIONS_TOKEN
 
 
261
  )
262
  meta = json.load(open(p))
263
+ if meta.get("username", "").lower() == username.lower():
264
  ts = meta.get("timestamp", 0)
265
  sub_date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
266
  if sub_date == today:
 
271
  except Exception:
272
  return 0
273
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
  def _validate_submission_json(obj: Any) -> Tuple[bool, str]:
275
  if not isinstance(obj, list):
276
  return False, "Submission must be a JSON list of annotations."
 
277
  required_keys = {"image_id", "score", "category_id", "area", "bbox", "segmentation"}
 
278
  for i, ann in enumerate(obj):
279
  if not isinstance(ann, dict):
280
  return False, f"Annotation at index {i} must be an object/dict."
 
291
  return False, f"area at index {i} must be a number."
292
  bbox = ann["bbox"]
293
  if not (isinstance(bbox, list) and len(bbox) == 4 and all(isinstance(x, (int, float)) for x in bbox)):
294
+ return False, f"bbox at index {i} must be a list of 4 numbers."
295
  if not isinstance(ann["segmentation"], list):
296
  return False, f"segmentation at index {i} must be a list."
 
297
  return True, "OK"
298
 
 
299
  def _upload_json(data: Any, path_in_repo: str, commit_message: str = "") -> None:
300
  with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
301
  json.dump(data, tmp, ensure_ascii=False)
 
315
  except OSError:
316
  pass
317
 
318
+ def _create_submission_record(*, pred, team, model_name, phase_codename,
319
+ challenge_type, original_filename, username, email) -> str:
 
 
 
 
 
 
 
 
 
 
320
  _require_token()
 
321
  submission_id = str(uuid.uuid4())
322
  ts = int(time.time())
 
323
  meta = {
324
  "submission_id": submission_id,
325
  "team": team.strip(),
 
328
  "challenge_type": challenge_type,
329
  "timestamp": ts,
330
  "original_filename": original_filename,
331
+ "username": username,
332
+ "email": email,
333
  }
 
334
  status = {"state": "queued", "timestamp": ts}
335
  base = f"submissions/{submission_id}"
 
336
  _upload_json(pred, f"{base}/pred.json", f"pred {submission_id}")
337
  _upload_json(meta, f"{base}/meta.json", f"meta {submission_id}")
338
  _upload_json(status, f"{base}/status.json", f"status {submission_id}")
 
339
  return submission_id
340
 
 
341
  def _load_leaderboard_df() -> pd.DataFrame:
342
  _require_token()
343
  try:
344
  path = hf_hub_download(
345
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
346
+ filename="leaderboard.jsonl", token=SUBMISSIONS_TOKEN
 
 
347
  )
348
  except HfHubHTTPError as e:
349
  if "404" in str(e):
 
368
  for col in ["team", "model", "phase_codename", "timestamp", *LEADERBOARD_METRICS]:
369
  if col not in df.columns:
370
  df[col] = None
 
371
  if DEFAULT_SORT_METRIC in df.columns:
372
  df = df.sort_values(by=DEFAULT_SORT_METRIC, ascending=False, kind="mergesort")
 
373
  return df
374
 
375
+ def _load_user_submissions(username: str) -> List[Dict]:
 
 
376
  _require_token()
377
  try:
378
  files = api_client().list_repo_files(
 
392
  filename=f, token=SUBMISSIONS_TOKEN
393
  )
394
  meta = json.load(open(meta_path))
395
+ if meta.get("username", "").lower() != username.lower():
 
396
  continue
 
 
397
  try:
398
  status_path = hf_hub_download(
399
  repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
 
402
  status = json.load(open(status_path))
403
  except Exception:
404
  status = {"state": "unknown"}
405
+ metrics = status.get("metrics", {}) if status.get("state") == "done" else {}
 
 
 
 
 
406
  results.append({
407
  "submission_id": sid,
408
  "team": meta.get("team", ""),
 
419
  results.sort(key=lambda x: x["timestamp"], reverse=True)
420
  return results
421
 
 
422
  # =========================
423
  # UI PAGES
424
  # =========================
 
427
  with st.expander("ℹ️ Overview of the VizWiz Benchmark Arena"):
428
  st.markdown("""
429
  **VizWiz Object Localization Challenge** β€” Submit your model predictions and get scored automatically.
 
430
  - πŸ“€ **Submit**: Upload your prediction JSON for Dev, Standard, or Challenge phases
431
  - πŸ† **Leaderboard**: View public rankings for the Standard phase
432
  - πŸ“‹ **My Submissions**: Track all your past submissions and scores
 
437
  except Exception:
438
  pass
439
 
 
440
  def render_eval_details():
441
  with st.expander("πŸ“ How is the Score Calculated?"):
442
  st.markdown("""
443
  Your submission is evaluated automatically against hidden ground-truth annotations.
 
444
  - **bbox_mAP** β€” bounding box mean average precision
445
  - **bbox_AP50** β€” bounding box AP at IoU=0.50
446
+ - **segm_mAP** β€” segmentation mean average precision
447
  - **segm_AP50** β€” segmentation AP at IoU=0.50 *(default ranking)*
448
  """)
449
 
 
450
  def page_leaderboard():
451
  st.header("πŸ† Leaderboard β€” Standard Phase")
452
  st.caption(f"Ranked by **{DEFAULT_SORT_METRIC}** (descending). Showing Standard phase submissions only.")
 
458
  st.error(f"Could not load leaderboard: {e}")
459
  return
460
 
 
461
  if not df.empty and "phase_codename" in df.columns:
462
  df = df[df["phase_codename"] == "test-standard2024"]
463
 
 
469
  df_display.insert(0, "Rank", range(1, len(df_display) + 1))
470
  if "timestamp" in df_display.columns:
471
  df_display["timestamp"] = pd.to_datetime(df_display["timestamp"], unit="s", errors="coerce")
472
+ for col in ["username", "email", "phase_codename", "submission_id"]:
 
 
473
  if col in df_display.columns:
474
  df_display.drop(columns=[col], inplace=True)
475
 
 
477
  df_display,
478
  column_config={
479
  "Rank": st.column_config.Column("Rank", width="small"),
480
+ "team": "Team", "model": "Model",
 
481
  "bbox_mAP": st.column_config.NumberColumn("bbox_mAP", format="%.4f"),
482
  "bbox_AP50": st.column_config.NumberColumn("bbox_AP50", format="%.4f"),
483
  "segm_mAP": st.column_config.NumberColumn("segm_mAP", format="%.4f"),
484
  "segm_AP50": st.column_config.NumberColumn("segm_AP50", format="%.4f"),
485
  "timestamp": st.column_config.DatetimeColumn("Scored at", format="D MMM YYYY, h:mm a"),
486
  },
487
+ use_container_width=True, hide_index=True,
 
488
  )
489
 
 
490
  def page_submit():
491
  user = render_login_wall()
492
+ username = user["username"]
 
493
 
494
  st.header("πŸš€ Submit Your Predictions")
495
 
 
496
  col_info, col_cap = st.columns([3, 1])
497
  with col_info:
498
+ st.markdown(f"πŸ‘€ Logged in as **{username}** ({user.get('team', '')})")
 
 
 
 
 
 
 
 
 
 
499
  with col_cap:
500
  with st.spinner("Checking daily limit..."):
501
+ subs_today = _count_submissions_today(username)
502
  remaining = DAILY_SUBMISSION_CAP - subs_today
503
  color = "green" if remaining > 1 else ("orange" if remaining == 1 else "red")
504
  st.markdown(
 
509
  )
510
 
511
  st.markdown("---")
 
512
  if remaining <= 0:
513
  st.error(f"β›” You've reached your daily limit of {DAILY_SUBMISSION_CAP} submissions. Come back tomorrow!")
514
  return
515
 
516
  col1, col2 = st.columns([2, 1])
 
517
  with col2:
518
  st.subheader("Submission Info")
519
+ team = st.text_input("Team / Display Name", value=st.session_state.get("team", user.get("team", "")))
520
  model_name = st.text_input("Model Name", value=st.session_state.get("model_name", ""))
521
  phase_label = st.selectbox("Phase", [p["label"] for p in PHASES])
522
  phase_codename = next(p["codename"] for p in PHASES if p["label"] == phase_label)
523
  challenge_type = st.radio("Challenge type", CHALLENGE_TYPES, horizontal=False)
524
  st.session_state["team"] = team
525
  st.session_state["model_name"] = model_name
 
526
 
527
  with col1:
528
  st.subheader("Upload Submission File")
529
  uploaded_file = st.file_uploader("Choose a JSON file", type=["json"])
 
530
  if uploaded_file is None:
531
  st.info("Upload a JSON file containing a list of annotations.")
532
  return
 
533
  try:
534
  raw = uploaded_file.getvalue().decode("utf-8")
535
  pred_obj = json.loads(raw)
536
  except Exception:
537
+ st.error("Could not parse JSON.")
538
  return
 
539
  ok, msg = _validate_submission_json(pred_obj)
540
  if not ok:
541
  st.error(f"Invalid submission format: {msg}")
542
  return
 
543
  st.success("Submission file looks valid βœ…")
544
 
545
+ if st.button("Submit (Queue for Evaluation)", type="primary"):
 
 
546
  if not team.strip():
547
  st.error("Please enter a Team / Display Name.")
548
  return
549
  if not model_name.strip():
550
  st.error("Please enter a Model Name.")
551
  return
552
+ if _count_submissions_today(username) >= DAILY_SUBMISSION_CAP:
553
+ st.error("β›” Daily submission limit reached.")
 
 
 
554
  return
 
555
  with st.spinner("Uploading submission..."):
556
  try:
557
  submission_id = _create_submission_record(
558
+ pred=pred_obj, team=team, model_name=model_name,
559
+ phase_codename=phase_codename, challenge_type=challenge_type,
 
 
 
560
  original_filename=uploaded_file.name,
561
+ username=username, email=user.get("email", ""),
 
562
  )
563
  except Exception as e:
564
  st.error(f"Upload failed: {e}")
565
  return
 
566
  st.balloons()
567
  st.success("Submission queued successfully!")
568
  st.code(f"Submission ID: {submission_id}")
 
 
569
 
570
  def page_my_submissions():
571
  user = render_login_wall()
572
+ username = user["username"]
573
 
574
  st.header("πŸ“‹ My Submissions")
575
+ st.caption(f"Showing all submissions for **{username}**")
576
 
577
  with st.spinner("Loading your submissions..."):
578
+ submissions = _load_user_submissions(username)
579
 
580
  if not submissions:
581
+ st.info("No submissions yet. Head to **Submit Model** to get started!")
582
  return
583
 
 
584
  phases_present = sorted(set(s["phase"] for s in submissions))
585
  selected_phase = st.selectbox("Filter by phase", ["All"] + phases_present)
 
586
  filtered = submissions if selected_phase == "All" else [s for s in submissions if s["phase"] == selected_phase]
587
 
588
+ state_colors = {"queued": "🟑", "running": "πŸ”΅", "done": "🟒", "failed": "πŸ”΄", "unknown": "βšͺ"}
 
 
 
 
 
 
 
 
589
  df = pd.DataFrame(filtered)
 
590
  if "timestamp" in df.columns:
591
  df["submitted_at"] = pd.to_datetime(df["timestamp"], unit="s", errors="coerce")
 
592
  if "state" in df.columns:
593
  df["status"] = df["state"].apply(lambda s: f"{state_colors.get(s, 'βšͺ')} {s.capitalize()}")
594
 
595
  display_cols = ["submitted_at", "status", "team", "model", "phase", "challenge_type"]
596
  metric_cols = [m for m in LEADERBOARD_METRICS if m in df.columns]
597
  display_cols += metric_cols
 
598
  df_display = df[[c for c in display_cols if c in df.columns]].copy()
599
 
600
  col_config = {
601
  "submitted_at": st.column_config.DatetimeColumn("Submitted At", format="D MMM YYYY, h:mm a"),
602
+ "status": "Status", "team": "Team", "model": "Model",
603
+ "phase": "Phase", "challenge_type": "Challenge Type",
 
 
 
604
  }
605
  for m in metric_cols:
606
  col_config[m] = st.column_config.NumberColumn(m, format="%.4f")
607
 
608
  st.dataframe(df_display, column_config=col_config, use_container_width=True, hide_index=True)
609
 
 
610
  st.markdown("---")
611
  total = len(submissions)
612
  done = sum(1 for s in submissions if s["state"] == "done")
 
614
  1 for s in submissions
615
  if datetime.fromtimestamp(s["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d") == _today_utc_str()
616
  )
 
617
  c1, c2, c3 = st.columns(3)
618
  c1.metric("Total Submissions", total)
619
  c2.metric("Scored", done)
620
  c3.metric(f"Today ({DAILY_SUBMISSION_CAP} max)", f"{today_count}/{DAILY_SUBMISSION_CAP}")
621
 
 
622
  # =========================
623
  # MAIN
624
  # =========================
625
 
626
  def main():
627
  st.sidebar.title("VizWiz Benchmark πŸ†")
628
+ user = get_logged_in_user()
 
 
629
 
630
  if user:
631
+ st.sidebar.markdown(f"πŸ‘€ **{user['username']}**")
632
+ st.sidebar.caption(user.get("team", ""))
633
+ if st.sidebar.button("Logout", use_container_width=True):
634
+ logout_user()
635
+ st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
636
  st.sidebar.markdown("---")
637
  menu = ["Leaderboard", "Submit Model", "My Submissions"]
638
  else:
639
+ menu = ["Leaderboard", "Login", "Sign Up"]
 
 
 
 
 
 
 
 
 
640
 
641
  choice = st.sidebar.radio("Navigation", menu)
642
  st.sidebar.markdown("---")
 
648
 
649
  if choice == "Leaderboard":
650
  page_leaderboard()
651
+ elif choice == "Login":
652
+ if "auth_page" not in st.session_state:
653
+ st.session_state["auth_page"] = "login"
654
+ if st.session_state["auth_page"] == "signup":
655
+ page_signup()
656
+ else:
657
+ page_login()
658
+ elif choice == "Sign Up":
659
+ page_signup()
660
  elif choice == "Submit Model":
661
  page_submit()
662
  elif choice == "My Submissions":
663
  page_my_submissions()
664
 
 
665
  if __name__ == "__main__":
666
  main()