ha251 commited on
Commit
19e34fa
·
verified ·
1 Parent(s): a06411a

Update miniapp_leaderboard.py

Browse files
Files changed (1) hide show
  1. miniapp_leaderboard.py +109 -643
miniapp_leaderboard.py CHANGED
@@ -4,725 +4,191 @@ import json
4
  import os
5
  import re
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
417
-
418
-
419
- APP_NAME = "miniapp"
420
-
421
- # 在 Space 里通过 Secrets 配置:
422
- # - HF_TOKEN: 具有写 dataset 权限的 token(Settings -> Variables and secrets -> Secrets)
423
- # - LEADERBOARD_DATASET: 形如 "your-username/miniapp-leaderboard"(repo_type=dataset)
424
- HF_TOKEN = os.environ.get("HF_TOKEN") or os.environ.get("TOKEN") or os.environ.get("HUGGINGFACE_TOKEN")
425
- LEADERBOARD_DATASET = os.environ.get("LEADERBOARD_DATASET", "").strip()
426
- # Owner 审核口令(放到 Space Secrets;不要放到公开 Variables)
427
- OWNER_REVIEW_TOKEN = os.environ.get("OWNER_REVIEW_TOKEN", "").strip()
428
-
429
- # 判断是否运行在 Hugging Face Spaces
430
- IN_SPACES = bool(
431
- os.environ.get("SPACE_ID")
432
- or os.environ.get("SPACE_REPO_NAME")
433
- or os.environ.get("SPACE_AUTHOR_NAME")
434
- or os.environ.get("system", "") == "spaces"
435
- )
436
-
437
- MAX_ENTRIES = int(os.environ.get("MAX_ENTRIES", "200"))
438
-
439
- PENDING_PREFIX = "pending/"
440
- APPROVED_PREFIX = "approved/"
441
-
442
-
443
- def _is_valid_http_url(url: str) -> bool:
444
- try:
445
- parsed = urlparse(url)
446
- return parsed.scheme in ("http", "https") and bool(parsed.netloc)
447
- except Exception:
448
- return False
449
-
450
-
451
- def _slug(s: str, max_len: int = 60) -> str:
452
- s = (s or "").strip().lower()
453
- s = re.sub(r"[^a-z0-9]+", "-", s)
454
- s = re.sub(r"-{2,}", "-", s).strip("-")
455
- return (s[:max_len] or "model")
456
-
457
-
458
- def _api() -> HfApi:
459
- return HfApi(token=HF_TOKEN)
460
-
461
-
462
- def _ensure_dataset_repo():
463
- if not HF_TOKEN:
464
- raise RuntimeError("未配置 HF_TOKEN(Space Secrets)。")
465
- if not LEADERBOARD_DATASET:
466
- raise RuntimeError("未配置 LEADERBOARD_DATASET(例如:your-username/miniapp-leaderboard)。")
467
- api = _api()
468
- try:
469
- api.repo_info(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
470
- except Exception:
471
- # 不存在则创建(public dataset;你也可以手动创建并设为 private)
472
- api.create_repo(repo_id=LEADERBOARD_DATASET, repo_type="dataset", private=False, exist_ok=True)
473
-
474
-
475
- def _empty_df() -> pd.DataFrame:
476
- return pd.DataFrame(columns=["submitted_at", "username", "model_name", "model_api", "notes"])
477
-
478
-
479
- def _list_json_files(prefix: str) -> list[str]:
480
- if not HF_TOKEN or not LEADERBOARD_DATASET:
481
- return []
482
-
483
- api = _api()
484
- try:
485
- files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
486
- except Exception:
487
- return []
488
-
489
- return sorted(
490
- [f for f in files if f.startswith(prefix) and f.endswith(".json")],
491
- reverse=True,
492
- )[:MAX_ENTRIES]
493
-
494
-
495
- def _load_entries_df(prefix: str, include_filename: bool) -> pd.DataFrame:
496
- files = _list_json_files(prefix)
497
- rows = []
498
- for filename in files:
499
- try:
500
- path = hf_hub_download(
501
- repo_id=LEADERBOARD_DATASET,
502
- repo_type="dataset",
503
- filename=filename,
504
- token=HF_TOKEN,
505
- )
506
- with open(path, "r", encoding="utf-8") as fp:
507
- row = json.load(fp)
508
- if include_filename:
509
- row["_filename"] = filename
510
- rows.append(row)
511
- except Exception:
512
- continue
513
-
514
- if not rows:
515
- df = _empty_df()
516
- if include_filename:
517
- df["_filename"] = []
518
- return df
519
-
520
- df = pd.DataFrame(rows)
521
- for col in ["submitted_at", "username", "model_name", "model_api", "notes"]:
522
- if col not in df.columns:
523
- df[col] = ""
524
- cols = ["submitted_at", "username", "model_name", "model_api", "notes"]
525
- if include_filename:
526
- if "_filename" not in df.columns:
527
- df["_filename"] = ""
528
- cols = cols + ["_filename"]
529
- df = df[cols]
530
- df = df.sort_values(by=["submitted_at"], ascending=False, kind="stable")
531
- return df
532
-
533
-
534
- def refresh():
535
- return _load_entries_df(APPROVED_PREFIX, include_filename=False)
536
-
537
-
538
- def refresh_pending():
539
- df = _load_entries_df(PENDING_PREFIX, include_filename=True)
540
- choices = list(df["_filename"]) if "_filename" in df.columns else []
541
- return df, gr.update(choices=choices, value=choices[0] if choices else None)
542
-
543
-
544
- def submit(model_name: str, model_api: str, notes: str, username: str | None):
545
- model_name = (model_name or "").strip()
546
- model_api = (model_api or "").strip()
547
- notes = (notes or "").strip()
548
- username = (username or "").strip() or ("local" if not IN_SPACES else "anonymous")
549
-
550
- if not model_name:
551
- return "请填写 **模型名称**。", refresh()
552
- if not model_api:
553
- return "请填写 **模型 API**。", refresh()
554
- if not _is_valid_http_url(model_api):
555
- return "**模型 API** 需要是合法的 `http(s)://...` URL。", refresh()
556
-
557
- if not HF_TOKEN:
558
- return "Space 未配置 **HF_TOKEN**(Secrets),���法写入排行榜。", refresh()
559
- if not LEADERBOARD_DATASET:
560
- return "Space 未配置 **LEADERBOARD_DATASET**(例如:`your-username/miniapp-leaderboard`)。", refresh()
561
-
562
- _ensure_dataset_repo()
563
- api = _api()
564
-
565
- now = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
566
- safe_model = _slug(model_name)
567
- safe_user = _slug(username)
568
- nonce = uuid.uuid4().hex[:8]
569
- path_in_repo = f"{PENDING_PREFIX}{now[:10]}/{now}-{safe_user}-{safe_model}-{nonce}.json"
570
-
571
- payload = {
572
- "submitted_at": now,
573
- "username": username,
574
- "model_name": model_name,
575
- "model_api": model_api,
576
- "notes": notes,
577
- }
578
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
579
- bio = io.BytesIO(data)
580
-
581
  api.upload_file(
582
  repo_id=LEADERBOARD_DATASET,
583
  repo_type="dataset",
584
- path_or_fileobj=bio,
585
- path_in_repo=path_in_repo,
586
- commit_message=f"miniapp: submit(pending) {username}/{model_name}",
587
- token=HF_TOKEN,
588
  )
589
 
590
- return "已提交,等待 owner 审核后才会上榜。", refresh()
591
 
592
 
593
- def approve(pending_filename: str | None, review_token: str | None):
594
- pending_filename = (pending_filename or "").strip()
595
- review_token = (review_token or "").strip()
596
 
597
- if not pending_filename:
598
- df, dd = refresh_pending()
599
- return "请选择一条待审核提交。", df, dd, refresh()
600
 
601
- if not OWNER_REVIEW_TOKEN:
602
- df, dd = refresh_pending()
603
- return "Space 未配置 **OWNER_REVIEW_TOKEN**(Secrets),无法启用审核。", df, dd, refresh()
 
 
 
604
 
605
- if review_token != OWNER_REVIEW_TOKEN:
606
- df, dd = refresh_pending()
607
- return "审核口令不正确。", df, dd, refresh()
 
608
 
609
- if not HF_TOKEN or not LEADERBOARD_DATASET:
610
- df, dd = refresh_pending()
611
- return "Space 未配置 HF 写入权限,无法审核。", df, dd, refresh()
 
 
 
 
612
 
613
- api = _api()
614
- try:
615
- local_path = hf_hub_download(
616
- repo_id=LEADERBOARD_DATASET,
617
- repo_type="dataset",
618
- filename=pending_filename,
619
- token=HF_TOKEN,
620
- )
621
- with open(local_path, "r", encoding="utf-8") as fp:
622
- payload = json.load(fp)
623
- except Exception as e:
624
- df, dd = refresh_pending()
625
- return f"读取待审核文件失败:{e}", df, dd, refresh()
626
-
627
- # 将文件复制到 approved 目录(保留原提交时间等字段)
628
- base = pending_filename[len(PENDING_PREFIX) :] if pending_filename.startswith(PENDING_PREFIX) else pending_filename
629
- approved_filename = f"{APPROVED_PREFIX}{base}"
630
- data = (json.dumps(payload, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
631
- bio = io.BytesIO(data)
632
 
633
- try:
634
- api.upload_file(
635
- repo_id=LEADERBOARD_DATASET,
636
- repo_type="dataset",
637
- path_or_fileobj=bio,
638
- path_in_repo=approved_filename,
639
- commit_message=f"miniapp: approve {payload.get('username','')}/{payload.get('model_name','')}",
640
- token=HF_TOKEN,
641
- )
642
- # 删除 pending 原文件(真正“上榜前审核”)
643
- api.delete_file(
644
- repo_id=LEADERBOARD_DATASET,
645
- repo_type="dataset",
646
- path_in_repo=pending_filename,
647
- commit_message=f"miniapp: remove pending {pending_filename}",
648
- token=HF_TOKEN,
649
- )
650
- except Exception as e:
651
- df, dd = refresh_pending()
652
- return f"审核写入失败:{e}", df, dd, refresh()
653
-
654
- pending_df, pending_dd = refresh_pending()
655
- approved_df = refresh()
656
- return "已通过审核并更新榜单。", pending_df, pending_dd, approved_df
657
 
658
-
659
- with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
660
  gr.Markdown(
661
- f"## {APP_NAME} leaderboard\n\n"
662
- "用户提交信息后会进入 **pending(待审核)**;owner 审核通过后才会进入 **approved(上榜)**。\n\n"
663
- f"- 当前 `LEADERBOARD_DATASET`: `{LEADERBOARD_DATASET or '(未配置)'}`\n"
 
 
 
664
  )
665
 
666
- with gr.Row():
667
- with gr.Column(scale=2):
668
- model_name = gr.Textbox(label="模型名称(必填)", placeholder="例如:my-agent-v1")
669
- model_api = gr.Textbox(
670
- label="模型 API(必填)",
671
- placeholder="例如:https://api.example.com/v1/chat/completions",
672
- )
673
- notes = gr.Textbox(label="备注(可选)", lines=4)
674
- username = gr.Textbox(
675
- label="用户名(可选)",
676
- placeholder="建议填你的 HF 用户名(也可留空)",
677
- value="" if IN_SPACES else "local",
678
- )
679
- submit_btn = gr.Button("提交", variant="primary")
680
- status = gr.Markdown()
681
- with gr.Column(scale=3):
682
- leaderboard = gr.Dataframe(
683
- label="Leaderboard(按提交时间倒序)",
684
- value=_load_entries_df(APPROVED_PREFIX, include_filename=False),
685
- interactive=False,
686
- wrap=True,
687
- )
688
- refresh_btn = gr.Button("刷新")
689
 
690
  submit_btn.click(
691
  submit,
692
- inputs=[model_name, model_api, notes, username],
693
  outputs=[status, leaderboard],
694
  )
695
- refresh_btn.click(refresh, inputs=[], outputs=[leaderboard])
696
-
697
- with gr.Accordion("Owner 审核(测试 demo)", open=False):
698
- gr.Markdown(
699
- "需要在 Space Secrets 中配置 `OWNER_REVIEW_TOKEN`。只有输入正确口令,才允许把 pending 放入榜单。"
700
- )
701
- review_token = gr.Textbox(label="审核口令", type="password", placeholder="OWNER_REVIEW_TOKEN")
702
- pending_refresh_btn = gr.Button("刷新待审核列表")
703
- pending_df = gr.Dataframe(
704
- label="待审核(pending)",
705
- value=_load_entries_df(PENDING_PREFIX, include_filename=True),
706
- interactive=False,
707
- wrap=True,
708
- )
709
- pending_pick = gr.Dropdown(
710
- label="选择要通过的提交(文件)",
711
- choices=_list_json_files(PENDING_PREFIX),
712
- )
713
- approve_btn = gr.Button("通过审核并上榜", variant="primary")
714
- approve_status = gr.Markdown()
715
-
716
- pending_refresh_btn.click(
717
- refresh_pending,
718
- inputs=[],
719
- outputs=[pending_df, pending_pick],
720
- )
721
- approve_btn.click(
722
- approve,
723
- inputs=[pending_pick, review_token],
724
- outputs=[approve_status, pending_df, pending_pick, leaderboard],
725
- )
726
-
727
- demo.launch()
728
 
 
 
4
  import os
5
  import re
6
  import uuid
 
7
 
8
  import gradio as gr
9
  import pandas as pd
 
10
  from huggingface_hub import HfApi, hf_hub_download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
+ APP_NAME = "MiniApp"
 
 
 
 
 
13
 
14
+ HF_TOKEN = os.environ.get("HF_TOKEN")
15
+ LEADERBOARD_DATASET = os.environ.get("LEADERBOARD_DATASET", "").strip()
16
+ OWNER_REVIEW_TOKEN = os.environ.get("OWNER_REVIEW_TOKEN", "").strip()
17
 
18
+ PENDING_PREFIX = "pending/"
19
+ APPROVED_PREFIX = "approved/"
 
20
 
21
+ COLUMNS = [
22
+ "model name",
23
+ "model family",
24
+ "avg",
25
+ "easy",
26
+ "mid",
27
+ "hard",
28
+ "submitted_at",
29
+ ]
30
 
31
+ NUMERIC_COLS = ["avg", "easy", "mid", "hard"]
32
 
 
 
 
 
 
33
 
34
+ def _api():
35
+ return HfApi(token=HF_TOKEN)
36
 
 
 
37
 
38
+ def _slug(s: str):
39
+ s = re.sub(r"[^a-z0-9]+", "-", (s or "").lower())
40
+ return s.strip("-") or "model"
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ def _load_df(prefix: str):
44
+ if not HF_TOKEN or not LEADERBOARD_DATASET:
45
+ return pd.DataFrame(columns=COLUMNS)
46
 
 
 
 
 
47
  api = _api()
48
  try:
49
  files = api.list_repo_files(repo_id=LEADERBOARD_DATASET, repo_type="dataset")
50
  except Exception:
51
+ return pd.DataFrame(columns=COLUMNS)
 
 
 
 
52
 
53
+ files = [f for f in files if f.startswith(prefix) and f.endswith(".json")]
54
+ rows = []
55
 
56
+ for f in files:
 
 
 
 
 
 
57
  try:
58
  path = hf_hub_download(
59
  repo_id=LEADERBOARD_DATASET,
60
  repo_type="dataset",
61
+ filename=f,
62
  token=HF_TOKEN,
63
  )
64
+ with open(path, "r") as fp:
65
+ rows.append(json.load(fp))
 
66
  except Exception:
67
  continue
68
 
69
  if not rows:
70
+ return pd.DataFrame(columns=COLUMNS)
71
 
72
  df = pd.DataFrame(rows)
73
+ for c in COLUMNS:
74
  if c not in df.columns:
75
  df[c] = ""
76
+
77
  for c in NUMERIC_COLS:
78
  df[c] = pd.to_numeric(df[c], errors="coerce")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
+ df = df.sort_values(by="avg", ascending=False)
81
+ return df[COLUMNS]
 
 
82
 
83
 
84
+ def refresh():
85
+ return _load_df(APPROVED_PREFIX)
 
86
 
87
 
88
+ def submit(model_name, model_family, email, zip_file):
89
+ if not model_name or not model_family or not email or zip_file is None:
90
+ return "All fields are required.", refresh()
 
 
 
 
91
 
92
+ if not zip_file.name.endswith(".zip"):
93
+ return "Please upload a .zip file.", refresh()
94
 
95
+ now = datetime.datetime.utcnow().isoformat() + "Z"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  safe_model = _slug(model_name)
97
+ nonce = uuid.uuid4().hex[:6]
98
+
99
+ json_path = f"{PENDING_PREFIX}{now}-{safe_model}-{nonce}.json"
100
+ zip_path = f"{PENDING_PREFIX}{now}-{safe_model}-{nonce}.zip"
101
 
 
102
  payload = {
103
+ "model name": model_name,
104
+ "model family": model_family,
105
+ "avg": 0,
106
+ "easy": 0,
107
+ "mid": 0,
108
+ "hard": 0,
109
+ "submitted_at": now,
110
+ "email": email,
 
 
 
 
 
 
111
  }
112
 
113
  api = _api()
114
+
115
+ # 上传 JSON
116
  api.upload_file(
117
  repo_id=LEADERBOARD_DATASET,
118
  repo_type="dataset",
119
+ path_or_fileobj=io.BytesIO(
120
+ json.dumps(payload, indent=2).encode("utf-8")
121
+ ),
122
+ path_in_repo=json_path,
123
+ commit_message=f"submit {model_name}",
124
  )
125
 
126
+ # 上传 ZIP
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  api.upload_file(
128
  repo_id=LEADERBOARD_DATASET,
129
  repo_type="dataset",
130
+ path_or_fileobj=zip_file,
131
+ path_in_repo=zip_path,
132
+ commit_message=f"upload zip {model_name}",
 
133
  )
134
 
135
+ return "Submitted. Waiting for review.", refresh()
136
 
137
 
138
+ with gr.Blocks(title=f"{APP_NAME} leaderboard") as demo:
 
 
139
 
140
+ gr.Markdown(f"# {APP_NAME} Leaderboard")
 
 
141
 
142
+ # ===============================
143
+ # Leaderboard 说明(占位符)
144
+ # ===============================
145
+ gr.Markdown(
146
+ """
147
+ ## Leaderboard Description
148
 
149
+ _Placeholder text: describe your benchmark, evaluation protocol,
150
+ scoring methodology, and ranking rules here._
151
+ """
152
+ )
153
 
154
+ # 排行榜(占满横向)
155
+ leaderboard = gr.Dataframe(
156
+ value=_load_df(APPROVED_PREFIX),
157
+ interactive=False,
158
+ wrap=True,
159
+ height=500,
160
+ )
161
 
162
+ refresh_btn = gr.Button("Refresh")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
+ # ===============================
165
+ # Submission 区域
166
+ # ===============================
167
+ gr.Markdown("---")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
 
 
169
  gr.Markdown(
170
+ """
171
+ ## Submission Instructions
172
+
173
+ _Placeholder text: explain submission format, evaluation pipeline,
174
+ review policy, and contact information here._
175
+ """
176
  )
177
 
178
+ model_name = gr.Textbox(label="Model name")
179
+ model_family = gr.Textbox(label="Model family")
180
+ email = gr.Textbox(label="Email")
181
+ zip_file = gr.File(label="Upload zip", file_types=[".zip"])
182
+
183
+ submit_btn = gr.Button("Submit", variant="primary")
184
+ status = gr.Markdown()
185
+
186
+ refresh_btn.click(refresh, outputs=[leaderboard])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
  submit_btn.click(
189
  submit,
190
+ inputs=[model_name, model_family, email, zip_file],
191
  outputs=[status, leaderboard],
192
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
+ demo.launch()