ha251 commited on
Commit
d7161b3
·
verified ·
1 Parent(s): 21ba29e

Update miniapp_leaderboard.py

Browse files
Files changed (1) hide show
  1. miniapp_leaderboard.py +405 -0
miniapp_leaderboard.py CHANGED
@@ -6,6 +6,411 @@ import re
6
  import uuid
7
  from urllib.parse import urlparse
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  import gradio as gr
10
  import pandas as pd
11
  from huggingface_hub import HfApi, hf_hub_download
 
6
  import uuid
7
  from urllib.parse import urlparse
8
 
9
+ import gradio as gr
10
+ import pandas as pd
11
+ import requests
12
+ from huggingface_hub import HfApi, hf_hub_download
13
+ from huggingface_hub.errors import RepositoryNotFoundError
14
+
15
+
16
+ APP_NAME = "miniapp"
17
+
18
+ # Space Secrets:
19
+ # - HF_TOKEN: Hugging Face access token with WRITE access to the dataset repo
20
+ # - LEADERBOARD_DATASET: dataset repo id like "ha251/miniapp-leaderboard"
21
+ HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
22
+ LEADERBOARD_DATASET = (os.environ.get("LEADERBOARD_DATASET") or "").strip()
23
+ MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "500"))
24
+
25
+ ENTRIES_PREFIX = "entries/"
26
+
27
+ LEADERBOARD_COLUMNS = [
28
+ "Model name",
29
+ "Avg",
30
+ "Easy",
31
+ "Mid",
32
+ "Hard",
33
+ "Games",
34
+ "Science",
35
+ "Tools",
36
+ "Humanities",
37
+ "Viz",
38
+ "Lifestyle",
39
+ "Submitted at",
40
+ "Submitter",
41
+ ]
42
+
43
+ NUMERIC_COLS = [
44
+ "Avg",
45
+ "Easy",
46
+ "Mid",
47
+ "Hard",
48
+ "Games",
49
+ "Science",
50
+ "Tools",
51
+ "Humanities",
52
+ "Viz",
53
+ "Lifestyle",
54
+ ]
55
+
56
+ IN_SPACES = bool(
57
+ os.environ.get("SPACE_ID")
58
+ or os.environ.get("SPACE_REPO_NAME")
59
+ or os.environ.get("SPACE_AUTHOR_NAME")
60
+ or os.environ.get("system", "") == "spaces"
61
+ )
62
+
63
+
64
+ def _api() -> HfApi:
65
+ return HfApi(token=HF_TOKEN)
66
+
67
+
68
+ def _is_valid_http_url(url: str) -> bool:
69
+ try:
70
+ parsed = urlparse(url)
71
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
72
+ except Exception:
73
+ return False
74
+
75
+
76
+ def _slug(s: str, max_len: int = 60) -> str:
77
+ s = (s or "").strip().lower()
78
+ s = re.sub(r"[^a-z0-9]+", "-", s)
79
+ s = re.sub(r"-{2,}", "-", s).strip("-")
80
+ return (s[:max_len] or "x")
81
+
82
+
83
+ def _empty_df() -> pd.DataFrame:
84
+ return pd.DataFrame(columns=LEADERBOARD_COLUMNS)
85
+
86
+
87
+ def _ensure_dataset_readable() -> tuple[bool, str]:
88
+ if not HF_TOKEN:
89
+ return False, "Space is missing HF_TOKEN (Secrets)."
90
+ if not LEADERBOARD_DATASET:
91
+ return False, "Space is missing LEADERBOARD_DATASET (Secrets)."
92
+ api = _api()
93
+ try:
94
+ api.repo_info(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
95
+ return True, ""
96
+ except RepositoryNotFoundError:
97
+ return False, (
98
+ f"Dataset repo not found: {LEADERBOARD_DATASET}. "
99
+ "Create it first (as a dataset) or fix LEADERBOARD_DATASET."
100
+ )
101
+ except Exception:
102
+ return False, "Cannot access the dataset repo. Check token permissions."
103
+
104
+
105
+ def _list_entry_files() -> list[str]:
106
+ ok, _ = _ensure_dataset_readable()
107
+ if not ok:
108
+ return []
109
+ api = _api()
110
+ try:
111
+ files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
112
+ except Exception:
113
+ return []
114
+
115
+ entry_files = [f for f in files if f.startswith(ENTRIES_PREFIX) and f.endswith(".json")]
116
+ entry_files.sort(reverse=True)
117
+ return entry_files[:MAX_ENTRIES]
118
+
119
+
120
+ def _load_entries_df() -> pd.DataFrame:
121
+ ok, _ = _ensure_dataset_readable()
122
+ if not ok:
123
+ return _empty_df()
124
+
125
+ rows: list[dict] = []
126
+ for filename in _list_entry_files():
127
+ try:
128
+ path = hf_hub_download(
129
+ repo_id=LEADERBOARD_DATASET,
130
+ repo_type="dataset",
131
+ filename=filename,
132
+ token=HF_TOKEN,
133
+ )
134
+ with open(path, "r", encoding="utf-8") as fp:
135
+ row = json.load(fp)
136
+ rows.append(row)
137
+ except Exception:
138
+ continue
139
+
140
+ if not rows:
141
+ return _empty_df()
142
+
143
+ df = pd.DataFrame(rows)
144
+ for c in LEADERBOARD_COLUMNS:
145
+ if c not in df.columns:
146
+ df[c] = ""
147
+ df = df[LEADERBOARD_COLUMNS]
148
+ for c in NUMERIC_COLS:
149
+ df[c] = pd.to_numeric(df[c], errors="coerce")
150
+ df = df.sort_values(by=["Submitted at"], ascending=False, kind="stable")
151
+ return df
152
+
153
+
154
+ def _apply_search_and_sort(
155
+ df: pd.DataFrame,
156
+ search_text: str,
157
+ search_in: str,
158
+ sort_by: str,
159
+ sort_order: str,
160
+ ) -> pd.DataFrame:
161
+ search_text = (search_text or "").strip().lower()
162
+ if search_text:
163
+ if search_in == "Model name":
164
+ df = df[df["Model name"].astype(str).str.lower().str.contains(search_text, na=False)]
165
+ elif search_in == "Submitter":
166
+ df = df[df["Submitter"].astype(str).str.lower().str.contains(search_text, na=False)]
167
+ else:
168
+ mask = (
169
+ df["Model name"].astype(str).str.lower().str.contains(search_text, na=False)
170
+ | df["Submitter"].astype(str).str.lower().str.contains(search_text, na=False)
171
+ )
172
+ df = df[mask]
173
+
174
+ ascending = sort_order == "Ascending"
175
+ if sort_by in df.columns:
176
+ df = df.sort_values(by=[sort_by], ascending=ascending, kind="stable", na_position="last")
177
+ return df
178
+
179
+
180
+ def refresh(search_text: str, search_in: str, sort_by: str, sort_order: str):
181
+ df = _load_entries_df()
182
+ return _apply_search_and_sort(df, search_text, search_in, sort_by, sort_order)
183
+
184
+
185
+ def _parse_hf_created_at(created_at: str) -> datetime.datetime | None:
186
+ try:
187
+ if created_at.endswith("Z"):
188
+ created_at = created_at[:-1] + "+00:00"
189
+ return datetime.datetime.fromisoformat(created_at)
190
+ except Exception:
191
+ return None
192
+
193
+
194
+ def _check_user_eligibility(username: str) -> tuple[bool, str]:
195
+ """
196
+ - Must be older than ~4 months (>= 120 days)
197
+ """
198
+ try:
199
+ r = requests.get(f"https://huggingface.co/api/users/{username}/overview", timeout=10)
200
+ r.raise_for_status()
201
+ created_at = r.json().get("createdAt")
202
+ if not created_at:
203
+ return False, "Cannot verify account creation date."
204
+ dt = _parse_hf_created_at(created_at)
205
+ if not dt:
206
+ return False, "Cannot parse account creation date."
207
+ now = datetime.datetime.now(datetime.timezone.utc)
208
+ if dt.tzinfo is None:
209
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
210
+ age_days = (now - dt).days
211
+ if age_days < 120:
212
+ return False, "Account must be older than 4 months to submit."
213
+ return True, ""
214
+ except Exception:
215
+ return False, "Cannot verify Hugging Face account. Please try again later."
216
+
217
+
218
+ def _submitted_today(username: str) -> bool:
219
+ df = _load_entries_df()
220
+ if df.empty:
221
+ return False
222
+ today = datetime.datetime.utcnow().date().isoformat()
223
+ user_rows = df[df["Submitter"].astype(str) == username]
224
+ if user_rows.empty:
225
+ return False
226
+ return any(str(v).startswith(today) for v in user_rows["Submitted at"].tolist())
227
+
228
+
229
+ def submit(
230
+ model_name: str,
231
+ model_api: str,
232
+ api_key: str,
233
+ avg: float,
234
+ easy: float,
235
+ mid: float,
236
+ hard: float,
237
+ games: float,
238
+ science: float,
239
+ tools: float,
240
+ humanities: float,
241
+ viz: float,
242
+ lifestyle: float,
243
+ search_text: str,
244
+ search_in: str,
245
+ sort_by: str,
246
+ sort_order: str,
247
+ profile: gr.OAuthProfile | None,
248
+ ):
249
+ # Login required to submit (in Spaces)
250
+ if IN_SPACES and (profile is None or not getattr(profile, "username", None)):
251
+ return "You must log in to submit.", refresh(search_text, search_in, sort_by, sort_order)
252
+
253
+ submitter = (getattr(profile, "username", None) if profile is not None else "local") or "anonymous"
254
+
255
+ model_name = (model_name or "").strip()
256
+ model_api = (model_api or "").strip()
257
+ api_key = (api_key or "").strip()
258
+
259
+ if not model_name:
260
+ return "Model name is required.", refresh(search_text, search_in, sort_by, sort_order)
261
+ if not model_api:
262
+ return "Model API URL is required.", refresh(search_text, search_in, sort_by, sort_order)
263
+ if not _is_valid_http_url(model_api):
264
+ return "Model API must be a valid http(s) URL.", refresh(search_text, search_in, sort_by, sort_order)
265
+ if not api_key:
266
+ return "API key is required.", refresh(search_text, search_in, sort_by, sort_order)
267
+
268
+ ok, msg = _ensure_dataset_readable()
269
+ if not ok:
270
+ return msg, refresh(search_text, search_in, sort_by, sort_order)
271
+
272
+ if IN_SPACES:
273
+ ok, msg = _check_user_eligibility(submitter)
274
+ if not ok:
275
+ return msg, refresh(search_text, search_in, sort_by, sort_order)
276
+
277
+ if _submitted_today(submitter):
278
+ return "You have already submitted today. Please try again tomorrow.", refresh(search_text, search_in, sort_by, sort_order)
279
+
280
+ now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
281
+ nonce = uuid.uuid4().hex[:8]
282
+ safe_model = _slug(model_name)
283
+ safe_user = _slug(submitter)
284
+ path_in_repo = f"{ENTRIES_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
285
+
286
+ # NOTE: api_key is collected but NOT stored.
287
+ payload = {
288
+ "Model name": model_name,
289
+ "Avg": avg,
290
+ "Easy": easy,
291
+ "Mid": mid,
292
+ "Hard": hard,
293
+ "Games": games,
294
+ "Science": science,
295
+ "Tools": tools,
296
+ "Humanities": humanities,
297
+ "Viz": viz,
298
+ "Lifestyle": lifestyle,
299
+ "Submitted at": now,
300
+ "Submitter": submitter,
301
+ "Model API": model_api,
302
+ }
303
+
304
+ api = _api()
305
+ data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
306
+ api.upload_file(
307
+ repo_id=LEADERBOARD_DATASET,
308
+ repo_type="dataset",
309
+ path_or_fileobj=io.BytesIO(data),
310
+ path_in_repo=path_in_repo,
311
+ commit_message=f"miniapp: submit {submitter}/{model_name}",
312
+ token=HF_TOKEN,
313
+ )
314
+
315
+ return "Submitted successfully.", refresh(search_text, search_in, sort_by, sort_order)
316
+
317
+
318
+ with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
319
+ # Layout: Overview -> Leaderboard -> Submission instructions
320
+ gr.Markdown(
321
+ f"## {APP_NAME} leaderboard\n\n"
322
+ "### Overview\n"
323
+ "_Placeholder overview text. Replace this with your benchmark description, rules, and links._\n\n"
324
+ "### Leaderboard\n"
325
+ "_The leaderboard below supports sorting and searching._\n\n"
326
+ "### Submission\n"
327
+ "_Placeholder submission instructions. Requires login. One submission per user per day. "
328
+ "Account must be older than 4 months._\n"
329
+ )
330
+
331
+ with gr.Row():
332
+ with gr.Column(scale=3):
333
+ with gr.Row():
334
+ search_text = gr.Textbox(label="Search", placeholder="Model name or submitter")
335
+ search_in = gr.Dropdown(
336
+ label="Search in",
337
+ choices=["Both", "Model name", "Submitter"],
338
+ value="Both",
339
+ )
340
+ with gr.Row():
341
+ sort_by = gr.Dropdown(label="Sort by", choices=LEADERBOARD_COLUMNS, value="Avg")
342
+ sort_order = gr.Radio(label="Order", choices=["Descending", "Ascending"], value="Descending")
343
+ refresh_btn = gr.Button("Refresh")
344
+
345
+ leaderboard = gr.Dataframe(
346
+ label="Leaderboard",
347
+ value=_load_entries_df(),
348
+ interactive=False,
349
+ wrap=True,
350
+ )
351
+
352
+ with gr.Column(scale=2):
353
+ with gr.Accordion("Submit (login required)", open=True):
354
+ model_name = gr.Textbox(label="Model name (required)")
355
+ model_api = gr.Textbox(label="Model API (required)", placeholder="https://...")
356
+ api_key = gr.Textbox(label="API key (required)", type="password", placeholder="Will not be stored")
357
+
358
+ with gr.Row():
359
+ avg = gr.Number(label="Avg", value=0)
360
+ easy = gr.Number(label="Easy", value=0)
361
+ mid = gr.Number(label="Mid", value=0)
362
+ hard = gr.Number(label="Hard", value=0)
363
+
364
+ with gr.Row():
365
+ games = gr.Number(label="Games", value=0)
366
+ science = gr.Number(label="Science", value=0)
367
+ tools = gr.Number(label="Tools", value=0)
368
+
369
+ with gr.Row():
370
+ humanities = gr.Number(label="Humanities", value=0)
371
+ viz = gr.Number(label="Viz", value=0)
372
+ lifestyle = gr.Number(label="Lifestyle", value=0)
373
+
374
+ with gr.Row():
375
+ gr.LoginButton()
376
+ submit_btn = gr.Button("Submit", variant="primary")
377
+ status = gr.Markdown()
378
+
379
+ refresh_btn.click(refresh, inputs=[search_text, search_in, sort_by, sort_order], outputs=[leaderboard])
380
+ submit_btn.click(
381
+ submit,
382
+ inputs=[
383
+ model_name,
384
+ model_api,
385
+ api_key,
386
+ avg,
387
+ easy,
388
+ mid,
389
+ hard,
390
+ games,
391
+ science,
392
+ tools,
393
+ humanities,
394
+ viz,
395
+ lifestyle,
396
+ search_text,
397
+ search_in,
398
+ sort_by,
399
+ sort_order,
400
+ ],
401
+ outputs=[status, leaderboard],
402
+ )
403
+
404
+ demo.launch()
405
+
406
+ import datetime
407
+ import io
408
+ import json
409
+ import os
410
+ import re
411
+ import uuid
412
+ from urllib.parse import urlparse
413
+
414
  import gradio as gr
415
  import pandas as pd
416
  from huggingface_hub import HfApi, hf_hub_download