NidhiS09 commited on
Commit
7ee2ab0
·
1 Parent(s): dbd5dbc

Restructure for challenges

Browse files
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: red
5
  colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.9.0
8
- app_file: main.py
9
  pinned: false
10
  hf_oauth: true
11
  hf_oauth_scopes:
 
5
  colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.9.0
8
+ app_file: app.py
9
  pinned: false
10
  hf_oauth: true
11
  hf_oauth_scopes:
app.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from challenges.home import build_home
4
+ from challenges.object_localization.page import build_ol_page
5
+ from challenges.answer_therapy.page import build_at_page
6
+ from challenges.vqa.page import build_vqa_page
7
+
8
+ with gr.Blocks() as demo:
9
+ build_home(demo)
10
+
11
+ build_ol_page(demo)
12
+ build_vqa_page(demo)
13
+ build_at_page(demo)
14
+
15
+ if __name__ == "__main__":
16
+ demo.launch()
auth_utils.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from hf_utils import _count_submissions_today, _get_cap_for_phase
4
+
5
+
6
+ def get_user_greeting(profile: gr.OAuthProfile | None) -> str:
7
+ if profile is None:
8
+ return "👋 Log in with your HuggingFace account to submit predictions."
9
+ return f"👤 Logged in as **{profile.username}**"
10
+
11
+
12
+ def get_daily_cap_info(profile: gr.OAuthProfile | None, phases: list = None) -> str:
13
+ if profile is None:
14
+ return ""
15
+ lines = []
16
+ for p in phases or []:
17
+ cap = _get_cap_for_phase(p["codename"])
18
+ used = _count_submissions_today(profile.username, p["codename"])
19
+ remaining = cap - used
20
+ lines.append(f"**{p['label'].split('(')[0].strip()}:** {remaining}/{cap} remaining")
21
+ return " \n".join(lines)
challenges/__init__.py ADDED
File without changes
challenges/answer_therapy/__init__.py ADDED
File without changes
challenges/answer_therapy/config.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PHASES = [
2
+ {"label": "Dev (test-dev2025)", "codename": "test-dev2025"},
3
+ {"label": "Standard (test-standard2025)", "codename": "test-standard2025"},
4
+ {"label": "Challenge (test-challenge2025)", "codename": "test-challenge2025"},
5
+ ]
6
+
7
+ LEADERBOARD_METRICS = [
8
+ "overall_f1", "overall_precision", "overall_recall",
9
+ "vizwiz_f1", "vizwiz_precision", "vizwiz_recall",
10
+ "vqav2_f1", "vqa_precision", "vqa_recall",
11
+ ]
12
+ DEFAULT_SORT_METRIC = "overall_f1"
13
+ LEADERBOARD_FILE = "leaderboards/answer-therapy.jsonl"
14
+ CHALLENGE_PHASE = "test-challenge2025"
15
+ SUBFOLDER = "answer-therapy"
16
+ CHALLENGE_TYPE = "VQA Answer Therapy"
17
+
18
+ EVAL_DETAILS_MD = """
19
+ ### How is the Score Calculated?
20
+
21
+ Each entry is a binary classification: does the visual question produce answers that all
22
+ share the **same image region** (single grounding), or do different answers point to
23
+ **different regions** (multiple groundings)?
24
+
25
+ Your `single_grounding` confidence score is thresholded at **0.5** for evaluation.
26
+
27
+ | Metric | Description |
28
+ |--------|-------------|
29
+ | `Overall F1` | F1 score across all questions *(default ranking metric)* |
30
+ | `Overall Precision` | Precision across all questions |
31
+ | `Overall Recall` | Recall across all questions |
32
+ | `VizWiz F1` | F1 on questions from the VizWiz dataset |
33
+ | `VQAv2 F1` | F1 on questions from the VQAv2 dataset |
34
+
35
+ Scores are reported as percentages (0–100).
36
+ """
37
+
38
+ FORMAT_MD = """
39
+ ### Submission Format
40
+
41
+ Your JSON file must be a **list of result objects**, one per visual question:
42
+
43
+ ```json
44
+ [
45
+ {
46
+ "question_id": "VizWiz_test_000000020000.jpg",
47
+ "single_grounding": 0.85
48
+ },
49
+ {
50
+ "question_id": "249549029",
51
+ "single_grounding": 0.12
52
+ },
53
+ ...
54
+ ]
55
+ ```
56
+
57
+ - **`question_id`** — string. Use the image filename for VizWiz questions (e.g. `VizWiz_test_00002183.jpg`) and the numeric string ID for VQAv2 questions.
58
+ - **`single_grounding`** — float between 0.0 and 1.0. Confidence that all answers share the same grounding region. `1` = single grounding, `0` = multiple groundings.
59
+ """
challenges/answer_therapy/page.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime, timezone
4
+
5
+ import gradio as gr
6
+ import pandas as pd
7
+
8
+ from auth_utils import get_user_greeting, get_daily_cap_info
9
+ from hf_utils import (
10
+ _load_leaderboard_df,
11
+ _load_user_submissions,
12
+ _create_submission_record,
13
+ _count_submissions_today,
14
+ _get_cap_for_phase,
15
+ _today_utc_str,
16
+ )
17
+ from config import DAILY_SUBMISSION_CAP, SUBMISSIONS_TOKEN
18
+ from challenges.answer_therapy.config import (
19
+ PHASES, LEADERBOARD_METRICS, DEFAULT_SORT_METRIC,
20
+ LEADERBOARD_FILE, CHALLENGE_PHASE, SUBFOLDER, CHALLENGE_TYPE,
21
+ EVAL_DETAILS_MD, FORMAT_MD,
22
+ )
23
+ from challenges.answer_therapy.validate import validate_submission
24
+
25
+
26
+ def load_leaderboard():
27
+ """Load VQA Answer Therapy leaderboard, filtered to challenge phase."""
28
+ try:
29
+ df = _load_leaderboard_df(LEADERBOARD_FILE, LEADERBOARD_METRICS)
30
+ except Exception as e:
31
+ return pd.DataFrame(), f"❌ Could not load leaderboard: {e}"
32
+
33
+ if not df.empty and "phase_codename" in df.columns:
34
+ df = df[df["phase_codename"] == CHALLENGE_PHASE]
35
+
36
+ if df.empty:
37
+ return pd.DataFrame(), "ℹ️ No scored Challenge phase submissions yet. Be the first!"
38
+
39
+ df = df.sort_values(by=DEFAULT_SORT_METRIC, ascending=False, kind="mergesort")
40
+ df_display = df.copy()
41
+ df_display.insert(0, "Rank", range(1, len(df_display) + 1))
42
+ if "timestamp" in df_display.columns:
43
+ df_display["Scored At"] = pd.to_datetime(
44
+ df_display["timestamp"], unit="s", errors="coerce"
45
+ ).dt.strftime("%d %b %Y, %I:%M %p")
46
+ df_display.drop(columns=["timestamp"], inplace=True)
47
+ for col in ["username", "email", "phase_codename", "submission_id"]:
48
+ if col in df_display.columns:
49
+ df_display.drop(columns=[col], inplace=True)
50
+
51
+ rename = {
52
+ "team": "Team", "model": "Model",
53
+ "overall_f1": "Overall F1",
54
+ "overall_precision": "Precision (Overall)",
55
+ "overall_recall": "Recall (Overall)",
56
+ "vizwiz_f1": "VizWiz F1",
57
+ "vizwiz_precision": "Precision (VizWiz)",
58
+ "vizwiz_recall": "Recall (VizWiz)",
59
+ "vqav2_f1": "VQAv2 F1",
60
+ "vqa_precision": "Precision (VQA)",
61
+ "vqa_recall": "Recall (VQA)",
62
+ }
63
+ df_display.rename(columns=rename, inplace=True)
64
+ return df_display, ""
65
+
66
+
67
+ def handle_submit(file, team, model_name, phase_label, profile: gr.OAuthProfile | None):
68
+ """Handle VQA Answer Therapy submission."""
69
+ if profile is None:
70
+ return "❌ You must be logged in with your HuggingFace account to submit.", ""
71
+ username = profile.username
72
+ email = getattr(profile, "email", "") or ""
73
+
74
+ if not SUBMISSIONS_TOKEN:
75
+ return "❌ Missing SUBMISSIONS_TOKEN. Add it in Space Settings → Secrets.", ""
76
+ if file is None:
77
+ return "❌ Please upload a JSON file.", ""
78
+ if not team.strip():
79
+ return "❌ Please enter a Team / Display Name.", ""
80
+ if not model_name.strip():
81
+ return "❌ Please enter a Model Name.", ""
82
+
83
+ phase_codename = next((p["codename"] for p in PHASES if p["label"] == phase_label), phase_label)
84
+ cap = _get_cap_for_phase(phase_codename)
85
+ subs_today = _count_submissions_today(username, phase_codename)
86
+ if subs_today >= cap:
87
+ phase_str = "challenge" if "challenge" in phase_codename else "this"
88
+ return f"⛔ You've reached your daily limit of {cap} submission(s) for the {phase_str} phase. Come back tomorrow!", ""
89
+
90
+ try:
91
+ with open(file, "r", encoding="utf-8") as f:
92
+ pred_obj = json.load(f)
93
+ except Exception:
94
+ return "❌ Could not parse JSON file.", ""
95
+
96
+ ok, msg = validate_submission(pred_obj)
97
+ if not ok:
98
+ return f"❌ Invalid submission format: {msg}", ""
99
+
100
+ original_filename = os.path.basename(file)
101
+ try:
102
+ submission_id = _create_submission_record(
103
+ pred=pred_obj,
104
+ team=team,
105
+ model_name=model_name,
106
+ phase_codename=phase_codename,
107
+ challenge_type=CHALLENGE_TYPE,
108
+ original_filename=original_filename,
109
+ username=username,
110
+ email=email,
111
+ subfolder=SUBFOLDER,
112
+ )
113
+ except Exception as e:
114
+ return f"❌ Upload failed: {e}", ""
115
+
116
+ remaining = cap - subs_today - 1
117
+ return (
118
+ f"✅ Submission queued! Visit **My Submissions** to track results. "
119
+ f"You have {remaining}/{cap} submissions remaining today for this phase.",
120
+ submission_id,
121
+ )
122
+
123
+
124
+ def load_my_submissions(phase_filter: str, profile: gr.OAuthProfile | None):
125
+ if profile is None:
126
+ return pd.DataFrame(), "❌ Please log in to view your submissions.", ""
127
+ username = profile.username
128
+ submissions = _load_user_submissions(username, subfolder=SUBFOLDER)
129
+
130
+ if not submissions:
131
+ return pd.DataFrame(), "ℹ️ No submissions yet. Head to Submit Predictions to get started!", ""
132
+
133
+ all_submissions = submissions[:]
134
+ if phase_filter and phase_filter != "All":
135
+ submissions = [s for s in submissions if s["phase"] == phase_filter]
136
+ if not submissions:
137
+ return pd.DataFrame(), f"ℹ️ No submissions found for phase **{phase_filter}**.", ""
138
+
139
+ state_icons = {"queued": "🟡", "running": "🔵", "done": "🟢", "failed": "🔴", "unknown": "⚪"}
140
+ df = pd.DataFrame(submissions)
141
+ if "timestamp" in df.columns:
142
+ df["Submitted At"] = pd.to_datetime(
143
+ df["timestamp"], unit="s", errors="coerce"
144
+ ).dt.strftime("%d %b %Y, %I:%M %p")
145
+ if "state" in df.columns:
146
+ df["Status"] = df["state"].apply(lambda s: f"{state_icons.get(s, '⚪')} {s.capitalize()}")
147
+
148
+ display_cols = ["Submitted At", "Status", "team", "model", "phase", "error"]
149
+ metric_cols = [m for m in LEADERBOARD_METRICS if m in df.columns]
150
+ display_cols += metric_cols
151
+ df_display = df[[c for c in display_cols if c in df.columns]].copy()
152
+ df_display.rename(columns={
153
+ "team": "Team", "model": "Model", "phase": "Phase", "error": "Error",
154
+ "overall_f1": "Overall F1", "overall_precision": "Precision", "overall_recall": "Recall",
155
+ "vizwiz_f1": "VizWiz F1", "vqav2_f1": "VQAv2 F1",
156
+ }, inplace=True)
157
+ for m in metric_cols:
158
+ if m in df_display.columns:
159
+ df_display[m] = df_display[m].apply(lambda x: f"{x:.2f}" if pd.notna(x) else "")
160
+
161
+ total = len(all_submissions)
162
+ done = sum(1 for s in all_submissions if s["state"] == "done")
163
+ today_count = sum(
164
+ 1 for s in all_submissions
165
+ if datetime.fromtimestamp(s["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d") == _today_utc_str()
166
+ )
167
+ stats = (
168
+ f"**Total:** {total}  |  "
169
+ f"**Scored:** {done}  |  "
170
+ f"**Today:** {today_count}/{DAILY_SUBMISSION_CAP}"
171
+ )
172
+ return df_display, "", stats
173
+
174
+
175
+ def build_at_page(demo: gr.Blocks) -> None:
176
+ with demo.route("VQA Answer Therapy", "/answer-therapy") as at_page:
177
+
178
+ with gr.Row():
179
+ with gr.Column(scale=5):
180
+ gr.Markdown("# 📍 Answer Therapy Challenge")
181
+ gr.Markdown(
182
+ "Predict whether answers to a visual question all share the same image region. "
183
+ "Evaluated with F1, Precision, and Recall across VizWiz and VQAv2 question sets."
184
+ )
185
+ with gr.Column(scale=1, min_width=160):
186
+ gr.LoginButton(size="lg")
187
+ at_greeting = gr.Markdown("👋 Log in to submit.")
188
+
189
+ gr.Markdown("---")
190
+
191
+ with gr.Tabs():
192
+
193
+ # ── Leaderboard ──
194
+ with gr.TabItem("🏆 Leaderboard"):
195
+ gr.Markdown("### Challenge Phase Rankings")
196
+ gr.Markdown("Ranked by **Overall F1** (descending). Challenge phase only.")
197
+ with gr.Accordion("📐 How is the Score Calculated?", open=False):
198
+ gr.Markdown(EVAL_DETAILS_MD)
199
+ at_lb_msg = gr.Markdown("")
200
+ at_lb_table = gr.Dataframe(interactive=False, wrap=True)
201
+ at_refresh_lb_btn = gr.Button("🔄 Refresh Leaderboard", variant="secondary", size="sm")
202
+
203
+ def refresh_at_leaderboard(profile: gr.OAuthProfile | None):
204
+ df, msg = load_leaderboard()
205
+ return df, msg, get_user_greeting(profile)
206
+
207
+ at_refresh_lb_btn.click(refresh_at_leaderboard, outputs=[at_lb_table, at_lb_msg, at_greeting])
208
+ at_page.load(refresh_at_leaderboard, outputs=[at_lb_table, at_lb_msg, at_greeting])
209
+
210
+ # ── Submit ──
211
+ with gr.TabItem("🚀 Submit Predictions"):
212
+ with gr.Row():
213
+ at_submit_greeting = gr.Markdown("👋 Log in with HuggingFace to submit.")
214
+ at_cap_info = gr.Markdown("")
215
+ gr.Markdown("---")
216
+ with gr.Row():
217
+ with gr.Column(scale=3):
218
+ gr.Markdown("#### Upload Submission File")
219
+ at_file_input = gr.File(label="Choose a JSON file", file_types=[".json"])
220
+ with gr.Accordion("📄 Submission Format", open=False):
221
+ gr.Markdown(FORMAT_MD)
222
+ with gr.Column(scale=2):
223
+ gr.Markdown("#### Submission Info")
224
+ at_team_input = gr.Textbox(label="Team / Display Name", placeholder="e.g. My Awesome Team")
225
+ at_model_input = gr.Textbox(label="Model Name", placeholder="e.g. CLIP-ViT-L")
226
+ at_phase_input = gr.Dropdown(
227
+ label="Phase",
228
+ choices=[p["label"] for p in PHASES],
229
+ value=PHASES[0]["label"],
230
+ )
231
+ at_submit_btn = gr.Button("Submit (Queue for Evaluation)", variant="primary", size="lg")
232
+ at_submit_status = gr.Markdown("")
233
+ at_sid_box = gr.Code(label="Submission ID", language=None, visible=False)
234
+
235
+ def do_at_submit(file, team, model_name, phase_label, profile: gr.OAuthProfile | None):
236
+ msg, sid = handle_submit(file, team, model_name, phase_label, profile)
237
+ return msg, gr.update(value=sid, visible=bool(sid))
238
+
239
+ at_submit_btn.click(
240
+ do_at_submit,
241
+ inputs=[at_file_input, at_team_input, at_model_input, at_phase_input],
242
+ outputs=[at_submit_status, at_sid_box],
243
+ )
244
+
245
+ def update_at_submit_ui(profile: gr.OAuthProfile | None):
246
+ return get_user_greeting(profile), get_daily_cap_info(profile, PHASES)
247
+
248
+ at_page.load(update_at_submit_ui, outputs=[at_submit_greeting, at_cap_info])
249
+
250
+ # ── My Submissions ──
251
+ with gr.TabItem("📋 My Submissions"):
252
+ at_my_sub_greeting = gr.Markdown("👋 Log in with HuggingFace to view your submissions.")
253
+ at_my_sub_stats = gr.Markdown("")
254
+ with gr.Row():
255
+ at_phase_filter = gr.Dropdown(
256
+ label="Filter by Phase",
257
+ choices=["All"] + [p["codename"] for p in PHASES],
258
+ value="All",
259
+ scale=2,
260
+ )
261
+ at_refresh_my_btn = gr.Button("🔄 Refresh", variant="secondary", scale=1)
262
+ at_my_sub_msg = gr.Markdown("")
263
+ at_my_sub_table = gr.Dataframe(interactive=False, wrap=True)
264
+
265
+ def refresh_at_my_subs(phase_filter, profile: gr.OAuthProfile | None):
266
+ df, msg, stats = load_my_submissions(phase_filter, profile)
267
+ return df, msg, stats, get_user_greeting(profile)
268
+
269
+ at_refresh_my_btn.click(
270
+ refresh_at_my_subs,
271
+ inputs=[at_phase_filter],
272
+ outputs=[at_my_sub_table, at_my_sub_msg, at_my_sub_stats, at_my_sub_greeting],
273
+ )
274
+ at_phase_filter.change(
275
+ refresh_at_my_subs,
276
+ inputs=[at_phase_filter],
277
+ outputs=[at_my_sub_table, at_my_sub_msg, at_my_sub_stats, at_my_sub_greeting],
278
+ )
279
+ at_page.load(
280
+ refresh_at_my_subs,
281
+ inputs=[at_phase_filter],
282
+ outputs=[at_my_sub_table, at_my_sub_msg, at_my_sub_stats, at_my_sub_greeting],
283
+ )
challenges/answer_therapy/validate.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Tuple
2
+
3
+
4
+ def validate_submission(obj: Any) -> Tuple[bool, str]:
5
+ """Validate VQA Answer Therapy submission format."""
6
+ if not isinstance(obj, list):
7
+ return False, "Submission must be a JSON list of result objects."
8
+ if len(obj) == 0:
9
+ return False, "Submission list is empty."
10
+ for i, item in enumerate(obj):
11
+ if not isinstance(item, dict):
12
+ return False, f"Entry at index {i} must be a JSON object."
13
+ if "question_id" not in item:
14
+ return False, f"Entry at index {i} missing 'question_id'."
15
+ if "single_grounding" not in item:
16
+ return False, f"Entry at index {i} missing 'single_grounding'."
17
+ if not isinstance(item["question_id"], str):
18
+ return False, f"'question_id' at index {i} must be a string."
19
+ sg = item["single_grounding"]
20
+ if not isinstance(sg, (int, float)):
21
+ return False, f"'single_grounding' at index {i} must be a float (0=multiple, 1=single)."
22
+ if not (0.0 <= float(sg) <= 1.0):
23
+ return False, f"'single_grounding' at index {i} must be between 0.0 and 1.0."
24
+ return True, "OK"
challenges/home.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+ from auth_utils import get_user_greeting
4
+
5
+ CHALLENGES = [
6
+ {
7
+ "id": "object-localization",
8
+ "title": "Object Localization",
9
+ "emoji": "🎯",
10
+ "description": "Detect and segment objects in images taken by blind photographers. Submit bounding box and instance segmentation predictions evaluated with pycocotools.",
11
+ "metrics": "bbox_mAP · bbox_AP50 · segm_mAP · segm_AP50",
12
+ "route": "/object-localization",
13
+ "active": True,
14
+ },
15
+ {
16
+ "id": "vqa",
17
+ "title": "Visual Question Answering",
18
+ "emoji": "🤔",
19
+ "description": "Answer open-ended questions about images taken by blind users. Models are evaluated on answer accuracy and relevance.",
20
+ "metrics": "Coming soon",
21
+ "route": "/vqa",
22
+ "active": True,
23
+ },
24
+ {
25
+ "id": "answer-therapy",
26
+ "title": "VQA Answer Therapy",
27
+ "emoji": "📍",
28
+ "description": "Predict whether a visual question produces answers that all share the same image region. Evaluated with F1, Precision, and Recall across VizWiz and VQAv2 subsets.",
29
+ "metrics": "Overall F1 · VizWiz F1 · VQAv2 F1",
30
+ "route": "/answer-therapy",
31
+ "active": True,
32
+ },
33
+ ]
34
+
35
+
36
+ def _challenge_card_html(c: dict) -> str:
37
+ if c["active"]:
38
+ return f"""
39
+ <div style="border:2px solid #2563eb;border-radius:12px;padding:24px;
40
+ background:#f0f7ff;display:flex;flex-direction:column;height:260px;box-sizing:border-box;">
41
+ <div style="font-size:2rem;margin-bottom:8px;">{c['emoji']}</div>
42
+ <h3 style="margin:0 0 6px 0;color:#1e40af;font-size:1rem;font-weight:700;">{c['title']}</h3>
43
+ <p style="margin:0 0 10px 0;color:#374151;font-size:0.82rem;line-height:1.45;flex:1;">{c['description']}</p>
44
+ <div style="background:#dbeafe;border-radius:5px;padding:4px 8px;
45
+ font-size:0.72rem;color:#1d4ed8;font-family:monospace;margin-bottom:14px;">
46
+ 📊 {c['metrics']}
47
+ </div>
48
+ <button onclick="window.location.href='{c['route']}'"
49
+ style="background:#2563eb;color:white;border:none;padding:8px 0;width:100%;
50
+ border-radius:7px;font-size:0.85rem;font-weight:600;cursor:pointer;">
51
+ Enter Challenge →
52
+ </button>
53
+ </div>"""
54
+ else:
55
+ return f"""
56
+ <div style="border:2px solid #e5e7eb;border-radius:12px;padding:24px;
57
+ background:#f9fafb;display:flex;flex-direction:column;height:260px;box-sizing:border-box;opacity:0.55;">
58
+ <div style="font-size:2rem;margin-bottom:8px;">{c['emoji']}</div>
59
+ <h3 style="margin:0 0 6px 0;color:#6b7280;font-size:1rem;font-weight:700;">{c['title']}</h3>
60
+ <p style="margin:0 0 10px 0;color:#9ca3af;font-size:0.82rem;line-height:1.45;flex:1;">{c['description']}</p>
61
+ <div style="background:#f3f4f6;border-radius:5px;padding:4px 8px;
62
+ font-size:0.72rem;color:#9ca3af;font-family:monospace;margin-bottom:14px;">
63
+ 📊 {c['metrics']}
64
+ </div>
65
+ <button disabled
66
+ style="background:#e5e7eb;color:#9ca3af;border:none;padding:8px 0;width:100%;
67
+ border-radius:7px;font-size:0.85rem;font-weight:600;cursor:not-allowed;">
68
+ 🔒 Coming Soon
69
+ </button>
70
+ </div>"""
71
+
72
+
73
+ def build_home(demo: gr.Blocks) -> None:
74
+ with gr.Row():
75
+ with gr.Column(scale=5):
76
+ gr.Markdown("# 🏆 VizWiz Benchmark Arena")
77
+ gr.Markdown(
78
+ "Automated evaluation platform for VizWiz challenges — "
79
+ "datasets collected from blind photographers using a smartphone app."
80
+ )
81
+ with gr.Column(scale=1, min_width=160):
82
+ gr.LoginButton(size="lg")
83
+ home_greeting = gr.Markdown("👋 Log in to submit.")
84
+
85
+ gr.Markdown("---")
86
+ gr.Markdown("## Challenges")
87
+ gr.Markdown(
88
+ "Choose a challenge to view its leaderboard, submit predictions, and track your results."
89
+ )
90
+ with gr.Row(equal_height=True):
91
+ for c in CHALLENGES:
92
+ with gr.Column(scale=1):
93
+ gr.HTML(_challenge_card_html(c))
94
+
95
+ gr.Markdown(
96
+ "---\n*More challenges coming soon. "
97
+ "All challenges use HuggingFace OAuth — log in once to access everything.*"
98
+ )
99
+ demo.load(get_user_greeting, outputs=[home_greeting])
challenges/object_localization/__init__.py ADDED
File without changes
challenges/object_localization/config.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ PHASES = [
2
+ {"label": "Dev (test-dev2024)", "codename": "test-dev2024"},
3
+ {"label": "Standard (test-standard2024)", "codename": "test-standard2024"},
4
+ {"label": "Challenge (test-challenge2024)", "codename": "test-challenge2024"},
5
+ ]
6
+
7
+ CHALLENGE_TYPES = ["Object Detection", "Instance Segmentation"]
8
+
9
+ LEADERBOARD_METRICS = ["bbox_mAP", "bbox_AP50", "segm_mAP", "segm_AP50"]
10
+ DEFAULT_SORT_METRIC = "segm_AP50"
11
+ LEADERBOARD_FILE = "leaderboard.jsonl"
12
+ CHALLENGE_PHASE = "test-challenge2024"
13
+
14
+ EVAL_DETAILS_MD = """
15
+ ### How is the Score Calculated?
16
+
17
+ Your submission is evaluated automatically against hidden ground-truth annotations using **pycocotools**.
18
+
19
+ | Metric | Description |
20
+ |--------|-------------|
21
+ | `bbox_mAP` | Bounding box mean average precision |
22
+ | `bbox_AP50` | Bounding box AP at IoU = 0.50 |
23
+ | `segm_mAP` | Segmentation mean average precision |
24
+ | `segm_AP50` | Segmentation AP at IoU = 0.50 *(default ranking metric)* |
25
+ """
26
+
27
+ FORMAT_MD = """
28
+ ### Submission Format
29
+
30
+ Your JSON file must be a **list of annotation objects**, each containing:
31
+
32
+ ```json
33
+ [
34
+ {
35
+ "image_id": 123,
36
+ "category_id": 101,
37
+ "score": 0.95,
38
+ "area": 1024.0,
39
+ "bbox": [x, y, width, height],
40
+ "segmentation": [[x1, y1, x2, y2, ...]]
41
+ },
42
+ ...
43
+ ]
44
+ ```
45
+ """
challenges/object_localization/page.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from datetime import datetime, timezone
4
+
5
+ import gradio as gr
6
+ import pandas as pd
7
+
8
+ from auth_utils import get_user_greeting, get_daily_cap_info
9
+ from hf_utils import (
10
+ _load_leaderboard_df,
11
+ _load_user_submissions,
12
+ _create_submission_record,
13
+ _count_submissions_today,
14
+ _get_cap_for_phase,
15
+ _today_utc_str,
16
+ )
17
+ from config import DAILY_SUBMISSION_CAP
18
+ from challenges.object_localization.config import (
19
+ PHASES, CHALLENGE_TYPES, LEADERBOARD_METRICS, DEFAULT_SORT_METRIC,
20
+ LEADERBOARD_FILE, CHALLENGE_PHASE, EVAL_DETAILS_MD, FORMAT_MD,
21
+ )
22
+ from challenges.object_localization.validate import validate_submission
23
+
24
+
25
+ def load_leaderboard():
26
+ try:
27
+ df = _load_leaderboard_df(LEADERBOARD_FILE, LEADERBOARD_METRICS)
28
+ except Exception as e:
29
+ return pd.DataFrame(), f"❌ Could not load leaderboard: {e}"
30
+
31
+ if not df.empty and "phase_codename" in df.columns:
32
+ df = df[df["phase_codename"] == CHALLENGE_PHASE]
33
+
34
+ if df.empty:
35
+ return pd.DataFrame(), "ℹ️ No scored Challenge phase submissions yet. Be the first!"
36
+
37
+ df = df.sort_values(by=DEFAULT_SORT_METRIC, ascending=False, kind="mergesort")
38
+ df_display = df.copy()
39
+ df_display.insert(0, "Rank", range(1, len(df_display) + 1))
40
+ if "timestamp" in df_display.columns:
41
+ df_display["Scored At"] = pd.to_datetime(
42
+ df_display["timestamp"], unit="s", errors="coerce"
43
+ ).dt.strftime("%d %b %Y, %I:%M %p")
44
+ df_display.drop(columns=["timestamp"], inplace=True)
45
+ for col in ["username", "email", "phase_codename", "submission_id"]:
46
+ if col in df_display.columns:
47
+ df_display.drop(columns=[col], inplace=True)
48
+ return df_display, ""
49
+
50
+
51
+ def handle_submit(file, team, model_name, phase_label, challenge_type, profile: gr.OAuthProfile | None):
52
+ if profile is None:
53
+ return "❌ You must be logged in with your HuggingFace account to submit.", ""
54
+ username = profile.username
55
+ email = getattr(profile, "email", "") or ""
56
+
57
+ from config import SUBMISSIONS_TOKEN
58
+ if not SUBMISSIONS_TOKEN:
59
+ return "❌ Missing SUBMISSIONS_TOKEN. Add it in Space Settings → Secrets.", ""
60
+ if file is None:
61
+ return "❌ Please upload a JSON file.", ""
62
+ if not team.strip():
63
+ return "❌ Please enter a Team / Display Name.", ""
64
+ if not model_name.strip():
65
+ return "❌ Please enter a Model Name.", ""
66
+
67
+ phase_codename = next((p["codename"] for p in PHASES if p["label"] == phase_label), phase_label)
68
+ cap = _get_cap_for_phase(phase_codename)
69
+ subs_today = _count_submissions_today(username, phase_codename)
70
+ if subs_today >= cap:
71
+ phase_label_str = "challenge" if phase_codename == "test-challenge2024" else "this"
72
+ return f"⛔ You've reached your daily limit of {cap} submission(s) for the {phase_label_str} phase. Come back tomorrow!", ""
73
+
74
+ try:
75
+ with open(file, "r", encoding="utf-8") as f:
76
+ pred_obj = json.load(f)
77
+ except Exception:
78
+ return "❌ Could not parse JSON file.", ""
79
+
80
+ ok, msg = validate_submission(pred_obj)
81
+ if not ok:
82
+ return f"❌ Invalid submission format: {msg}", ""
83
+
84
+ original_filename = os.path.basename(file)
85
+ try:
86
+ submission_id = _create_submission_record(
87
+ pred=pred_obj, team=team, model_name=model_name,
88
+ phase_codename=phase_codename, challenge_type=challenge_type,
89
+ original_filename=original_filename, username=username, email=email,
90
+ )
91
+ except Exception as e:
92
+ return f"❌ Upload failed: {e}", ""
93
+
94
+ remaining = cap - subs_today - 1
95
+ return (
96
+ f"✅ Submission queued successfully! Visit **My Submissions** to see the results. "
97
+ f"You have {remaining}/{cap} submissions remaining today for this phase.",
98
+ submission_id,
99
+ )
100
+
101
+
102
+ def load_my_submissions(phase_filter: str, profile: gr.OAuthProfile | None):
103
+ if profile is None:
104
+ return pd.DataFrame(), "❌ Please log in to view your submissions.", ""
105
+ username = profile.username
106
+ submissions = _load_user_submissions(username)
107
+
108
+ if not submissions:
109
+ return pd.DataFrame(), "ℹ️ No submissions yet. Head to Submit Predictions to get started!", ""
110
+
111
+ all_submissions = submissions[:]
112
+ if phase_filter and phase_filter != "All":
113
+ submissions = [s for s in submissions if s["phase"] == phase_filter]
114
+ if not submissions:
115
+ return pd.DataFrame(), f"ℹ️ No submissions found for phase **{phase_filter}**.", ""
116
+
117
+ state_icons = {"queued": "🟡", "running": "🔵", "done": "🟢", "failed": "🔴", "unknown": "⚪"}
118
+ df = pd.DataFrame(submissions)
119
+ if "timestamp" in df.columns:
120
+ df["Submitted At"] = pd.to_datetime(
121
+ df["timestamp"], unit="s", errors="coerce"
122
+ ).dt.strftime("%d %b %Y, %I:%M %p")
123
+ if "state" in df.columns:
124
+ df["Status"] = df["state"].apply(lambda s: f"{state_icons.get(s, '⚪')} {s.capitalize()}")
125
+
126
+ display_cols = ["Submitted At", "Status", "team", "model", "phase", "challenge_type", "error"]
127
+ metric_cols = [m for m in LEADERBOARD_METRICS if m in df.columns]
128
+ display_cols += metric_cols
129
+ df_display = df[[c for c in display_cols if c in df.columns]].copy()
130
+ df_display.rename(columns={
131
+ "team": "Team", "model": "Model", "phase": "Phase",
132
+ "challenge_type": "Challenge Type", "error": "Error",
133
+ }, inplace=True)
134
+ for m in metric_cols:
135
+ if m in df_display.columns:
136
+ df_display[m] = df_display[m].apply(lambda x: f"{x:.4f}" if pd.notna(x) else "")
137
+
138
+ total = len(all_submissions)
139
+ done = sum(1 for s in all_submissions if s["state"] == "done")
140
+ today_count = sum(
141
+ 1 for s in all_submissions
142
+ if datetime.fromtimestamp(s["timestamp"], tz=timezone.utc).strftime("%Y-%m-%d") == _today_utc_str()
143
+ )
144
+ stats = (
145
+ f"**Total:** {total} &nbsp;|&nbsp; "
146
+ f"**Scored:** {done} &nbsp;|&nbsp; "
147
+ f"**Today:** {today_count}/{DAILY_SUBMISSION_CAP}"
148
+ )
149
+ return df_display, "", stats
150
+
151
+
152
+ def build_ol_page(demo: gr.Blocks) -> None:
153
+ with demo.route("Object Localization", "/object-localization") as obj_loc:
154
+
155
+ with gr.Row():
156
+ with gr.Column(scale=5):
157
+ gr.Markdown("# 🎯 Object Localization Challenge")
158
+ gr.Markdown(
159
+ "Submit bounding box and instance segmentation predictions "
160
+ "evaluated automatically against hidden ground-truth annotations."
161
+ )
162
+ with gr.Column(scale=1, min_width=160):
163
+ gr.LoginButton(size="lg")
164
+ ol_greeting = gr.Markdown("👋 Log in to submit.")
165
+
166
+ gr.Markdown("---")
167
+
168
+ with gr.Tabs():
169
+
170
+ # ── Leaderboard ──
171
+ with gr.TabItem("🏆 Leaderboard"):
172
+ gr.Markdown("### Challenge Phase Rankings")
173
+ gr.Markdown(f"Ranked by **{DEFAULT_SORT_METRIC}** (descending). Challenge phase only.")
174
+ with gr.Accordion("📐 How is the Score Calculated?", open=False):
175
+ gr.Markdown(EVAL_DETAILS_MD)
176
+ ol_lb_msg = gr.Markdown("")
177
+ ol_lb_table = gr.Dataframe(interactive=False, wrap=True)
178
+ ol_refresh_lb_btn = gr.Button("🔄 Refresh Leaderboard", variant="secondary", size="sm")
179
+
180
+ def refresh_ol_leaderboard(profile: gr.OAuthProfile | None):
181
+ df, msg = load_leaderboard()
182
+ return df, msg, get_user_greeting(profile)
183
+
184
+ ol_refresh_lb_btn.click(refresh_ol_leaderboard, outputs=[ol_lb_table, ol_lb_msg, ol_greeting])
185
+ obj_loc.load(refresh_ol_leaderboard, outputs=[ol_lb_table, ol_lb_msg, ol_greeting])
186
+
187
+ # ── Submit ──
188
+ with gr.TabItem("🚀 Submit Predictions"):
189
+ with gr.Row():
190
+ ol_submit_greeting = gr.Markdown("👋 Log in with HuggingFace to submit.")
191
+ ol_cap_info = gr.Markdown("")
192
+ gr.Markdown("---")
193
+ with gr.Row():
194
+ with gr.Column(scale=3):
195
+ gr.Markdown("#### Upload Submission File")
196
+ ol_file_input = gr.File(label="Choose a JSON file", file_types=[".json"])
197
+ with gr.Accordion("📄 Submission Format", open=False):
198
+ gr.Markdown(FORMAT_MD)
199
+ with gr.Column(scale=2):
200
+ gr.Markdown("#### Submission Info")
201
+ ol_team_input = gr.Textbox(label="Team / Display Name", placeholder="e.g. My Awesome Team")
202
+ ol_model_input = gr.Textbox(label="Model Name", placeholder="e.g. ResNet50-FPN")
203
+ ol_phase_input = gr.Dropdown(
204
+ label="Phase",
205
+ choices=[p["label"] for p in PHASES],
206
+ value=PHASES[0]["label"],
207
+ )
208
+ ol_challenge_input = gr.Radio(
209
+ label="Challenge Type",
210
+ choices=CHALLENGE_TYPES,
211
+ value=CHALLENGE_TYPES[0],
212
+ )
213
+ ol_submit_btn = gr.Button("Submit (Queue for Evaluation)", variant="primary", size="lg")
214
+ ol_submit_status = gr.Markdown("")
215
+ ol_sid_box = gr.Code(label="Submission ID", language=None, visible=False)
216
+
217
+ def do_ol_submit(file, team, model_name, phase_label, challenge_type, profile: gr.OAuthProfile | None):
218
+ msg, sid = handle_submit(file, team, model_name, phase_label, challenge_type, profile)
219
+ return msg, gr.update(value=sid, visible=bool(sid))
220
+
221
+ ol_submit_btn.click(
222
+ do_ol_submit,
223
+ inputs=[ol_file_input, ol_team_input, ol_model_input, ol_phase_input, ol_challenge_input],
224
+ outputs=[ol_submit_status, ol_sid_box],
225
+ )
226
+
227
+ def update_ol_submit_ui(profile: gr.OAuthProfile | None):
228
+ return get_user_greeting(profile), get_daily_cap_info(profile, PHASES)
229
+
230
+ obj_loc.load(update_ol_submit_ui, outputs=[ol_submit_greeting, ol_cap_info])
231
+
232
+ # ── My Submissions ──
233
+ with gr.TabItem("📋 My Submissions"):
234
+ ol_my_sub_greeting = gr.Markdown("👋 Log in with HuggingFace to view your submissions.")
235
+ ol_my_sub_stats = gr.Markdown("")
236
+ with gr.Row():
237
+ ol_phase_filter = gr.Dropdown(
238
+ label="Filter by Phase",
239
+ choices=["All"] + [p["codename"] for p in PHASES],
240
+ value="All",
241
+ scale=2,
242
+ )
243
+ ol_refresh_my_btn = gr.Button("🔄 Refresh", variant="secondary", scale=1)
244
+ ol_my_sub_msg = gr.Markdown("")
245
+ ol_my_sub_table = gr.Dataframe(interactive=False, wrap=True)
246
+
247
+ def refresh_ol_my_subs(phase_filter, profile: gr.OAuthProfile | None):
248
+ df, msg, stats = load_my_submissions(phase_filter, profile)
249
+ return df, msg, stats, get_user_greeting(profile)
250
+
251
+ ol_refresh_my_btn.click(
252
+ refresh_ol_my_subs,
253
+ inputs=[ol_phase_filter],
254
+ outputs=[ol_my_sub_table, ol_my_sub_msg, ol_my_sub_stats, ol_my_sub_greeting],
255
+ )
256
+ ol_phase_filter.change(
257
+ refresh_ol_my_subs,
258
+ inputs=[ol_phase_filter],
259
+ outputs=[ol_my_sub_table, ol_my_sub_msg, ol_my_sub_stats, ol_my_sub_greeting],
260
+ )
261
+ obj_loc.load(
262
+ refresh_ol_my_subs,
263
+ inputs=[ol_phase_filter],
264
+ outputs=[ol_my_sub_table, ol_my_sub_msg, ol_my_sub_stats, ol_my_sub_greeting],
265
+ )
challenges/object_localization/validate.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any, Tuple
2
+
3
+
4
+ def validate_submission(obj: Any) -> Tuple[bool, str]:
5
+ if not isinstance(obj, list):
6
+ return False, "Submission must be a JSON list of annotations."
7
+ required_keys = {"image_id", "score", "category_id", "area", "bbox", "segmentation"}
8
+ for i, ann in enumerate(obj):
9
+ if not isinstance(ann, dict):
10
+ return False, f"Annotation at index {i} must be an object/dict."
11
+ missing = required_keys - set(ann.keys())
12
+ if missing:
13
+ return False, f"Annotation at index {i} missing keys: {sorted(list(missing))}"
14
+ if not isinstance(ann["image_id"], int):
15
+ return False, f"image_id at index {i} must be an integer."
16
+ if not isinstance(ann["category_id"], int):
17
+ return False, f"category_id at index {i} must be an integer."
18
+ if not isinstance(ann["score"], (int, float)):
19
+ return False, f"score at index {i} must be a number."
20
+ if not isinstance(ann["area"], (int, float)):
21
+ return False, f"area at index {i} must be a number."
22
+ bbox = ann["bbox"]
23
+ if not (isinstance(bbox, list) and len(bbox) == 4
24
+ and all(isinstance(x, (int, float)) for x in bbox)):
25
+ return False, f"bbox at index {i} must be a list of 4 numbers."
26
+ if not isinstance(ann["segmentation"], list):
27
+ return False, f"segmentation at index {i} must be a list."
28
+ return True, "OK"
challenges/vqa/__init__.py ADDED
File without changes
challenges/vqa/config.py ADDED
File without changes
challenges/vqa/page.py ADDED
File without changes
challenges/vqa/validate.py ADDED
File without changes
config.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ # =========================
4
+ # DATABASE / HF REPO
5
+ # =========================
6
+
7
+ DB_REPO_ID = os.getenv("DB_REPO_ID", "VizWiz-Challenges/submissions-db")
8
+ DB_REPO_TYPE = "dataset"
9
+ SUBMISSIONS_TOKEN = os.getenv("SUBMISSIONS_TOKEN", "")
10
+
11
+ DAILY_SUBMISSION_CAP = 5
hf_utils.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import tempfile
4
+ import time
5
+ import uuid
6
+ from datetime import datetime, timezone
7
+ from typing import Any, Dict, List
8
+
9
+ import pandas as pd
10
+ from huggingface_hub import HfApi, hf_hub_download
11
+ from huggingface_hub.utils import HfHubHTTPError
12
+
13
+ from config import DB_REPO_ID, DB_REPO_TYPE, SUBMISSIONS_TOKEN, DAILY_SUBMISSION_CAP
14
+
15
+ # =========================
16
+ # HF API CLIENT
17
+ # =========================
18
+
19
+ _api = None
20
+ def api_client() -> HfApi:
21
+ global _api
22
+ if _api is None:
23
+ _api = HfApi()
24
+ return _api
25
+
26
+ # =========================
27
+ # DATE / CAP HELPERS
28
+ # =========================
29
+
30
+ def _today_utc_str() -> str:
31
+ return datetime.now(timezone.utc).strftime("%Y-%m-%d")
32
+
33
+ def _get_cap_for_phase(phase_codename: str) -> int:
34
+ """Return the daily submission cap for a given phase."""
35
+ if phase_codename in ("test-challenge2024", "test-challenge2025"):
36
+ return 1
37
+ return DAILY_SUBMISSION_CAP
38
+
39
+ def _count_submissions_today(username: str, phase_codename: str | None = None) -> int:
40
+ """Count today's submissions for a user, optionally filtered by phase."""
41
+ try:
42
+ files = api_client().list_repo_files(
43
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE, token=SUBMISSIONS_TOKEN
44
+ )
45
+ today = _today_utc_str()
46
+ count = 0
47
+ for f in files:
48
+ if not (f.startswith("submissions/") and f.endswith("/meta.json")):
49
+ continue
50
+ try:
51
+ p = hf_hub_download(
52
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
53
+ filename=f, token=SUBMISSIONS_TOKEN
54
+ )
55
+ meta = json.load(open(p))
56
+ if meta.get("username", "").lower() != username.lower():
57
+ continue
58
+ if phase_codename and meta.get("phase_codename") != phase_codename:
59
+ continue
60
+ ts = meta.get("timestamp", 0)
61
+ sub_date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
62
+ if sub_date == today:
63
+ count += 1
64
+ except Exception:
65
+ continue
66
+ return count
67
+ except Exception:
68
+ return 0
69
+
70
+ # =========================
71
+ # UPLOAD HELPERS
72
+ # =========================
73
+
74
+ def _upload_json(data: Any, path_in_repo: str, commit_message: str = "") -> None:
75
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as tmp:
76
+ json.dump(data, tmp, ensure_ascii=False)
77
+ tmp_path = tmp.name
78
+ try:
79
+ api_client().upload_file(
80
+ path_or_fileobj=tmp_path,
81
+ path_in_repo=path_in_repo,
82
+ repo_id=DB_REPO_ID,
83
+ repo_type=DB_REPO_TYPE,
84
+ token=SUBMISSIONS_TOKEN,
85
+ commit_message=commit_message or f"Add {path_in_repo}",
86
+ )
87
+ finally:
88
+ try:
89
+ os.remove(tmp_path)
90
+ except OSError:
91
+ pass
92
+
93
+ def _create_submission_record(*, pred, team, model_name, phase_codename,
94
+ challenge_type, original_filename, username, email,
95
+ subfolder: str = "") -> str:
96
+ """
97
+ Write pred.json / meta.json / status.json to the dataset repo.
98
+ subfolder: e.g. "answer-therapy" → submissions/answer-therapy/<uuid>/
99
+ """
100
+ if not SUBMISSIONS_TOKEN:
101
+ raise ValueError("Missing SUBMISSIONS_TOKEN.")
102
+ submission_id = str(uuid.uuid4())
103
+ ts = int(time.time())
104
+ meta = {
105
+ "submission_id": submission_id,
106
+ "team": team.strip(),
107
+ "model": model_name.strip(),
108
+ "phase_codename": phase_codename,
109
+ "challenge_type": challenge_type,
110
+ "timestamp": ts,
111
+ "original_filename": original_filename,
112
+ "username": username,
113
+ "email": email,
114
+ }
115
+ status = {"state": "queued", "timestamp": ts}
116
+ prefix = f"submissions/{subfolder}/{submission_id}" if subfolder else f"submissions/{submission_id}"
117
+ _upload_json(pred, f"{prefix}/pred.json", f"pred {submission_id}")
118
+ _upload_json(meta, f"{prefix}/meta.json", f"meta {submission_id}")
119
+ _upload_json(status, f"{prefix}/status.json", f"status {submission_id}")
120
+ return submission_id
121
+
122
+ # =========================
123
+ # READ HELPERS
124
+ # =========================
125
+
126
+ def _load_user_submissions(username: str, subfolder: str = "") -> List[Dict]:
127
+ """Load all submissions for a user from a given subfolder (or root)."""
128
+ if not SUBMISSIONS_TOKEN:
129
+ return []
130
+ try:
131
+ files = api_client().list_repo_files(
132
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE, token=SUBMISSIONS_TOKEN
133
+ )
134
+ except Exception:
135
+ return []
136
+
137
+ prefix = f"submissions/{subfolder}/" if subfolder else "submissions/"
138
+ results = []
139
+ for f in files:
140
+ if not (f.startswith(prefix) and f.endswith("/meta.json")):
141
+ continue
142
+ try:
143
+ parts = f.split("/")
144
+ sid = parts[2] if subfolder else parts[1]
145
+ meta_path = hf_hub_download(
146
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
147
+ filename=f, token=SUBMISSIONS_TOKEN
148
+ )
149
+ meta = json.load(open(meta_path))
150
+ if meta.get("username", "").lower() != username.lower():
151
+ continue
152
+ status_file = f"{prefix}{sid}/status.json"
153
+ try:
154
+ status_path = hf_hub_download(
155
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
156
+ filename=status_file, token=SUBMISSIONS_TOKEN
157
+ )
158
+ status = json.load(open(status_path))
159
+ except Exception:
160
+ status = {"state": "unknown"}
161
+ metrics = status.get("metrics", {}) if status.get("state") == "done" else {}
162
+ error = status.get("error", "") if status.get("state") == "failed" else ""
163
+ results.append({
164
+ "submission_id": sid,
165
+ "team": meta.get("team", ""),
166
+ "model": meta.get("model", ""),
167
+ "phase": meta.get("phase_codename", ""),
168
+ "challenge_type": meta.get("challenge_type", ""),
169
+ "timestamp": meta.get("timestamp", 0),
170
+ "state": status.get("state", "unknown"),
171
+ "error": error[:120] if error else "",
172
+ **metrics,
173
+ })
174
+ except Exception:
175
+ continue
176
+
177
+ results.sort(key=lambda x: x["timestamp"], reverse=True)
178
+ return results
179
+
180
+ def _load_leaderboard_df(leaderboard_file: str, metric_cols: list) -> pd.DataFrame:
181
+ """Generic leaderboard loader for any challenge."""
182
+ empty = pd.DataFrame(columns=["team", "model", "phase_codename", *metric_cols, "timestamp"])
183
+ if not SUBMISSIONS_TOKEN:
184
+ return empty
185
+ try:
186
+ path = hf_hub_download(
187
+ repo_id=DB_REPO_ID, repo_type=DB_REPO_TYPE,
188
+ filename=leaderboard_file, token=SUBMISSIONS_TOKEN
189
+ )
190
+ except HfHubHTTPError as e:
191
+ if "404" in str(e):
192
+ return empty
193
+ raise
194
+
195
+ rows = []
196
+ with open(path, "r", encoding="utf-8") as f:
197
+ for line in f:
198
+ line = line.strip()
199
+ if not line:
200
+ continue
201
+ try:
202
+ rows.append(json.loads(line))
203
+ except json.JSONDecodeError:
204
+ continue
205
+
206
+ if not rows:
207
+ return empty
208
+
209
+ df = pd.DataFrame(rows)
210
+ for col in ["team", "model", "phase_codename", "timestamp", *metric_cols]:
211
+ if col not in df.columns:
212
+ df[col] = None
213
+ return df
main.py → main_prev.py RENAMED
File without changes