NidhiS09 commited on
Commit
1b700f4
Β·
verified Β·
1 Parent(s): 65b5e2c

Create main.py

Browse files
Files changed (1) hide show
  1. main.py +667 -0
main.py ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ 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
11
+ from PIL import Image
12
+ from huggingface_hub import HfApi, hf_hub_download
13
+ from huggingface_hub.utils import HfHubHTTPError
14
+
15
+ # =========================
16
+ # CONFIG
17
+ # =========================
18
+
19
+ st.set_page_config(
20
+ page_title="VizWiz Benchmark Arena",
21
+ page_icon="πŸ†",
22
+ layout="wide",
23
+ initial_sidebar_state="expanded",
24
+ )
25
+
26
+ DB_REPO_ID = os.getenv("DB_REPO_ID", "NidhiS09/VizWiz-submissions-db")
27
+ DB_REPO_TYPE = "dataset"
28
+ SUBMISSIONS_TOKEN = os.getenv("SUBMISSIONS_TOKEN", "")
29
+
30
+ DAILY_SUBMISSION_CAP = 5
31
+
32
+ PHASES = [
33
+ {"label": "Dev (query-dev2024)", "codename": "test-dev2024"},
34
+ {"label": "Standard (query-standard2024)", "codename": "test-standard2024"},
35
+ {"label": "Challenge (query-challenge2024)", "codename": "test-challenge2024"},
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
119
+ )
120
+ today = _today_utc_str()
121
+ count = 0
122
+ for f in files:
123
+ if not (f.startswith("submissions/") and f.endswith("/meta.json")):
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:
137
+ count += 1
138
+ except Exception:
139
+ continue
140
+ return count
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."
171
+ missing = required_keys - set(ann.keys())
172
+ if missing:
173
+ return False, f"Annotation at index {i} missing keys: {sorted(list(missing))}"
174
+ if not isinstance(ann["image_id"], int):
175
+ return False, f"image_id at index {i} must be an integer."
176
+ if not isinstance(ann["category_id"], int):
177
+ return False, f"category_id at index {i} must be an integer."
178
+ if not isinstance(ann["score"], (int, float)):
179
+ return False, f"score at index {i} must be a number."
180
+ if not isinstance(ann["area"], (int, float)):
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)
194
+ tmp_path = tmp.name
195
+ try:
196
+ api_client().upload_file(
197
+ path_or_fileobj=tmp_path,
198
+ path_in_repo=path_in_repo,
199
+ repo_id=DB_REPO_ID,
200
+ repo_type=DB_REPO_TYPE,
201
+ token=SUBMISSIONS_TOKEN,
202
+ commit_message=commit_message or f"Add {path_in_repo}",
203
+ )
204
+ finally:
205
+ try:
206
+ os.remove(tmp_path)
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(),
230
+ "model": model_name.strip(),
231
+ "phase_codename": phase_codename,
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):
260
+ return pd.DataFrame(columns=["team", "model", "phase_codename", *LEADERBOARD_METRICS, "timestamp"])
261
+ raise
262
+
263
+ rows = []
264
+ with open(path, "r", encoding="utf-8") as f:
265
+ for line in f:
266
+ line = line.strip()
267
+ if not line:
268
+ continue
269
+ try:
270
+ rows.append(json.loads(line))
271
+ except json.JSONDecodeError:
272
+ continue
273
+
274
+ if not rows:
275
+ return pd.DataFrame(columns=["team", "model", "phase_codename", *LEADERBOARD_METRICS, "timestamp"])
276
+
277
+ df = pd.DataFrame(rows)
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(
293
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE, token=SUBMISSIONS_TOKEN
294
+ )
295
+ except Exception:
296
+ return []
297
+
298
+ results = []
299
+ for f in files:
300
+ if not (f.startswith("submissions/") and f.endswith("/meta.json")):
301
+ continue
302
+ try:
303
+ sid = f.split("/")[1]
304
+ meta_path = hf_hub_download(
305
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
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,
317
+ filename=f"submissions/{sid}/status.json", token=SUBMISSIONS_TOKEN
318
+ )
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", ""),
331
+ "model": meta.get("model", ""),
332
+ "phase": meta.get("phase_codename", ""),
333
+ "challenge_type": meta.get("challenge_type", ""),
334
+ "timestamp": meta.get("timestamp", 0),
335
+ "state": status.get("state", "unknown"),
336
+ **metrics,
337
+ })
338
+ except Exception:
339
+ continue
340
+
341
+ results.sort(key=lambda x: x["timestamp"], reverse=True)
342
+ return results
343
+
344
+
345
+ # =========================
346
+ # UI PAGES
347
+ # =========================
348
+
349
+ 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
357
+ """)
358
+ try:
359
+ overview_image = Image.open("src/overview_image.png").resize((600, 600))
360
+ st.image(overview_image, caption="Example of an object localization task")
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.")
380
+
381
+ with st.spinner("Loading leaderboard..."):
382
+ try:
383
+ df = _load_leaderboard_df()
384
+ except Exception as e:
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
+
392
+ if df.empty:
393
+ st.info("No scored Standard phase submissions yet. Be the first!")
394
+ return
395
+
396
+ df_display = df.copy()
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
+
406
+ st.dataframe(
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(
450
+ f'<div style="text-align:center;padding:8px;border-radius:8px;border:1px solid {color}">'
451
+ f'<b style="color:{color}">{remaining}/{DAILY_SUBMISSION_CAP}</b><br>'
452
+ f'<small>submissions left today</small></div>',
453
+ unsafe_allow_html=True,
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")
595
+ today_count = sum(
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("---")
652
+ st.sidebar.caption("Submissions are queued to a private DB repo and evaluated automatically.")
653
+
654
+ render_overview()
655
+ render_eval_details()
656
+ st.markdown("---")
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()