SeaWolf-AI commited on
Commit
cbc9da9
ยท
verified ยท
1 Parent(s): 8e3e99d

Sort leaderboards by each board primary metric; resident reference tools

Browse files

A classification-only board has no MAE, and sorting it by MAE raised a server error. Boards now sort by their own primary metric. Adds a reference-tool row between the baselines and entrant submissions; it carries no rank.

Files changed (3) hide show
  1. app.py +462 -425
  2. i18n.js +2 -2
  3. index.html +3 -2
app.py CHANGED
@@ -1,425 +1,462 @@
1
- # -*- coding: utf-8 -*-
2
- """LEADBOARD โ€” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ ๋ถ„์•ผ๋ณ„๋กœ ๊ฐ™์€ ์žฃ๋Œ€์— ์„ธ์šฐ๋Š” ์‹œํ—˜๋Œ€.
3
-
4
- **์ด ์„œ๋น„์Šค๊ฐ€ ํ•˜๋Š” ์ผ**
5
- ยท ์นดํ…Œ๊ณ ๋ฆฌยท๋ถ€๋ฌธ ์นด๋“œ๋ฅผ ์„œ๋น™ํ•œ๋‹ค
6
- ยท ํ…Œ์ŠคํŠธ์…‹(๊ตฌ์กฐ๋งŒ)์„ ๋‚ด๋ ค์ค€๋‹ค
7
- ยท ์ œ์ถœ์„ ๋ฐ›์•„ ๋น„๊ณต๊ฐœ ์›์žฅ์— ์ ๋Š”๋‹ค
8
- ยท ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ์ˆœ์œ„ํ‘œ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค
9
-
10
- **ํ•˜์ง€ ์•Š๋Š” ์ผ: ์ฑ„์ .**
11
- ์ •๋‹ต์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์—†๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์—ฌ๊ธฐ์— ๋ผ๋ฒจ์„ ๋‘๋ฉด
12
- ๊ทธ ์ˆœ๊ฐ„ 1์กฐ๊ฐ€ ๋ฌด๋„ˆ์ง„๋‹ค. ์ฑ„์ ์€ ์›์žฅ์„ ํด๋งํ•˜๋Š” ๋ณ„๋„ ์›Œ์ปค๊ฐ€ ๋กœ์ปฌ ์ •๋‹ต ํŒŒ์ผ๋กœ ํ•œ๋‹ค.
13
-
14
- ๋ถ€๋ฌธ ์นด๋“œ์—๋Š” **์ฑ„์ ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ณต๊ฐœ ์ •๋ณด**๊ฐ€ ๋“ค์–ด ์žˆ๋‹ค โ€”
15
- ๋ถ„ํ•  ๋“ฑ๊ธ‰ ยท ์ •๋‹ต ๋“ฑ๊ธ‰ ยท ์žก์Œ ๋ฐ”๋‹ฅ ยท ๊ธฐ์ค€์„  ์„ฑ์  ยท ๋ฐ์ดํ„ฐ ์ง€๋ฌธ.
16
- ์šฐ๋ฆฌ ์ ์ˆ˜๋ฅผ ๋ฏฟ์–ด๋‹ฌ๋ผ๊ณ  ํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ๋‹ค.
17
- """
18
- import base64
19
- import glob
20
- import hashlib
21
- import hmac
22
- import io
23
- import json
24
- import os
25
- import re
26
- import secrets
27
- import time
28
- import urllib.parse
29
- import urllib.request
30
-
31
- from fastapi import FastAPI, HTTPException, Request
32
- from fastapi.middleware.gzip import GZipMiddleware
33
- from fastapi.responses import (FileResponse, JSONResponse, RedirectResponse,
34
- Response)
35
- from pydantic import BaseModel
36
-
37
- HERE = os.path.dirname(os.path.abspath(__file__))
38
- DATA = os.path.join(HERE, "data")
39
- SPEC = "v1.1"
40
-
41
- LEDGER = os.environ.get("LB_LEDGER_REPO", "FINAL-Bench/leadboard-submissions")
42
- LEDGER_API = "https://huggingface.co/api/datasets/%s" % LEDGER
43
- LEDGER_RAW = "https://huggingface.co/datasets/%s/resolve/main" % LEDGER
44
- HF_TOKEN = os.environ.get("HF_TOKEN", "")
45
-
46
- OAUTH_ID = os.environ.get("OAUTH_CLIENT_ID", "")
47
- OAUTH_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
48
- OAUTH_ISS = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
49
- SPACE_HOST = os.environ.get("SPACE_HOST", "")
50
- COOKIE = "lb_session"
51
- IN_FRAME = bool(SPACE_HOST)
52
- COOKIE_KW = ({"samesite": "none", "secure": True} if IN_FRAME
53
- else {"samesite": "lax", "secure": False})
54
- SESSION_KEY = os.environ.get("LB_SESSION_KEY") or secrets.token_hex(16)
55
- DAILY_CAP = int(os.environ.get("LB_DAILY_CAP", "5"))
56
-
57
- app = FastAPI(title="LEADBOARD")
58
- app.add_middleware(GZipMiddleware, minimum_size=1024)
59
-
60
- _C = {}
61
- # ํ•˜๋ฃจ ํ•œ๋„ ์นด์šดํ„ฐ. ์ปจํ…Œ์ด๋„ˆ ์žฌ๊ธฐ๋™์ด๋ฉด ๋น„๋Š”๋ฐ, ๊ทธ๋ž˜๋„ ํญ์ฃผ๋Š” ๋ง‰๋Š”๋‹ค.
62
- # ์ •ํ™•ํ•œ ํšŒ๊ณ„๊ฐ€ ํ•„์š”ํ•ด์ง€๋ฉด ์›์žฅ ์ชฝ์œผ๋กœ ์˜ฎ๊ธด๋‹ค.
63
- _CAP = {}
64
-
65
-
66
- def cached(key, ttl, produce):
67
- hit = _C.get(key)
68
- if hit and time.time() - hit[0] < ttl:
69
- return hit[1]
70
- try:
71
- v = produce()
72
- except Exception:
73
- if hit:
74
- return hit[1]
75
- raise
76
- _C[key] = (time.time(), v)
77
- return v
78
-
79
-
80
- # ------------------------------------------------------------------ ๋ถ€๋ฌธ ์นด๋“œ
81
- def _sha(path):
82
- h = hashlib.sha256()
83
- with open(path, "rb") as f:
84
- for b in iter(lambda: f.read(65536), b""):
85
- h.update(b)
86
- return h.hexdigest()[:16]
87
-
88
-
89
- def _load_boards():
90
- out = {}
91
- for p in sorted(glob.glob(os.path.join(DATA, "*_card.json"))):
92
- try:
93
- d = json.load(io.open(p, encoding="utf-8"))
94
- except Exception:
95
- continue
96
- nf = d.get("noise_floor") or {}
97
- base = d.get("baselines") or {}
98
- # ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—๋Š” MAE ๊ธฐ์ค€์„ ์ด ์—†๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋Š” AUROC ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ๊ฒƒ์„ ์ตœ์„ ์œผ๋กœ ๋ณธ๋‹ค.
99
- # ์—†๋Š” ๊ฐ’์„ 0 ์œผ๋กœ ์น˜๋ฉด ๊ทธ ๋ถ€๋ฌธ์ด "๊ฐ€์žฅ ์ •ํ™•ํ•œ ๊ณณ"์œผ๋กœ ํ‘œ์— ์˜ค๋ฅธ๋‹ค.
100
- with_mae = {k: v for k, v in base.items() if v.get("mae") is not None}
101
- if with_mae:
102
- best = min(with_mae, key=lambda k: with_mae[k]["mae"])
103
- d["metric"] = "mae"
104
- elif base:
105
- best = max(base, key=lambda k: base[k].get("auroc") or 0)
106
- d["metric"] = "auroc"
107
- else:
108
- best = None
109
- d["metric"] = None
110
- d["dataset_sha"] = _sha(p)
111
- d["spec"] = SPEC
112
- d["best_baseline"] = best
113
- # ๋ชจ๋ธ ์˜ค์ฐจ๊ฐ€ ์‹คํ—˜ ์˜ค์ฐจ์˜ ๋ช‡ ๋ฐฐ์ธ๊ฐ€. 1 ์— ๋ถ™์„์ˆ˜๋ก ์ธก์ • ํ•œ๊ณ„๋‹ค.
114
- if best and nf.get("sd_single") and d["metric"] == "mae":
115
- d["error_over_noise"] = round(base[best]["mae"] / nf["sd_single"], 2)
116
- if ("์ƒ์ˆ˜ ์˜ˆ์ธก" in base and "Morgan+LightGBM" in base
117
- and base["Morgan+LightGBM"].get("mae") is not None
118
- and base["์ƒ์ˆ˜ ์˜ˆ์ธก"].get("mae") is not None):
119
- d["beats_constant"] = base["Morgan+LightGBM"]["mae"] < base["์ƒ์ˆ˜ ์˜ˆ์ธก"]["mae"]
120
- if d.get("n_test"):
121
- d["near_pct"] = round(100.0 * d.get("near_threshold", 0) / d["n_test"], 1)
122
- d["open"] = True
123
- out[d["board"]] = d
124
- return out
125
-
126
-
127
- def boards():
128
- return cached("boards", 300, _load_boards)
129
-
130
-
131
- def categories():
132
- def build():
133
- cats = json.load(io.open(os.path.join(HERE, "categories.json"), encoding="utf-8"))
134
- bd = boards()
135
- for c in cats["categories"]:
136
- c["open"] = len([b for b in c["boards"] if b in bd])
137
- c["board_cards"] = [bd[b] for b in c["boards"] if b in bd]
138
- return cats
139
- return cached("cats", 300, build)
140
-
141
-
142
- # ------------------------------------------------------------------ ์›์žฅ
143
- def _hdr():
144
- return {"Authorization": "Bearer " + HF_TOKEN, "User-Agent": "LEADBOARD/1.0"}
145
-
146
-
147
- def ledger_read(path, default=None):
148
- try:
149
- with urllib.request.urlopen(urllib.request.Request(
150
- "%s/%s" % (LEDGER_RAW, path), headers=_hdr()), timeout=60) as r:
151
- return json.loads(r.read())
152
- except Exception:
153
- return default
154
-
155
-
156
- def ledger_write(path, obj, summary):
157
- blob = base64.b64encode(json.dumps(obj, ensure_ascii=False).encode()).decode()
158
- lines = [json.dumps({"key": "header", "value": {"summary": summary}}),
159
- json.dumps({"key": "file", "value": {"path": path, "content": blob,
160
- "encoding": "base64"}})]
161
- req = urllib.request.Request(LEDGER_API + "/commit/main",
162
- data=("\n".join(lines) + "\n").encode(),
163
- headers=dict(_hdr(), **{"Content-Type": "application/x-ndjson"}))
164
- with urllib.request.urlopen(req, timeout=180) as r:
165
- return json.loads(r.read())
166
-
167
-
168
- # ------------------------------------------------------------------ ์„ธ์…˜
169
- def sign(v):
170
- return hmac.new(SESSION_KEY.encode(), v.encode(), hashlib.sha256).hexdigest()[:32]
171
-
172
-
173
- def set_session(resp, user):
174
- raw = json.dumps(user, ensure_ascii=False)
175
- b = base64.urlsafe_b64encode(raw.encode()).decode()
176
- resp.set_cookie(COOKIE, "%s.%s" % (b, sign(b)), max_age=86400 * 7,
177
- httponly=True, **COOKIE_KW)
178
-
179
-
180
- def who(req: Request):
181
- c = req.cookies.get(COOKIE) or ""
182
- if "." not in c:
183
- return None
184
- b, sg = c.rsplit(".", 1)
185
- if not hmac.compare_digest(sg, sign(b)):
186
- return None
187
- try:
188
- return json.loads(base64.urlsafe_b64decode(b.encode()).decode())
189
- except Exception:
190
- return None
191
-
192
-
193
- # ------------------------------------------------------------------ ๋ผ์šฐํŠธ
194
- @app.get("/")
195
- def index():
196
- return FileResponse(os.path.join(HERE, "index.html"))
197
-
198
-
199
- @app.get("/i18n.js")
200
- def i18n():
201
- """๋ฌธ์ž์—ด ์‚ฌ์ „. ํ™”๋ฉด ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌํ•ด ๋‘์–ด์•ผ ์˜์–ดํŒ์ด ์กฐ์šฉํžˆ ๋’ค์ฒ˜์ง€์ง€ ์•Š๋Š”๋‹ค."""
202
- return FileResponse(os.path.join(HERE, "i18n.js"),
203
- media_type="application/javascript")
204
-
205
-
206
- @app.get("/api/categories")
207
- def api_categories():
208
- c = categories()
209
- bd = boards()
210
- return {"spec": SPEC, "categories": c["categories"],
211
- "totals": {"planned": sum(x["planned"] for x in c["categories"]),
212
- "open": len(bd), "ledger": bool(HF_TOKEN)}}
213
-
214
-
215
- @app.get("/api/leaders")
216
- def api_leaders():
217
- """๋ถ€๋ฌธ๋งˆ๋‹ค ํ˜„์žฌ 1์œ„ ํ•œ ์ค„. ์ฒซ ํ™”๋ฉด์—์„œ ์ „์ฒด๋ฅผ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ์œ„ํ•œ ๊ฒƒ์ด๋‹ค.
218
-
219
- ๋ถ€๋ฌธ๋ณ„๋กœ ์ˆœ์œ„ํ‘œ๋ฅผ ๋”ฐ๋กœ ๋ถ€๋ฅด๋ฉด ์™•๋ณต์ด ๋ถ€๋ฌธ ์ˆ˜๋งŒํผ ๋Š˜์–ด๋‚œ๋‹ค. ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ์— ๋ชจ์€๋‹ค.
220
- **์ฐธ๊ฐ€ ์ œ์ถœ์ด ์—†์œผ๋ฉด ๋น„์›Œ์„œ ๋ณด๋‚ธ๋‹ค** - ๊ธฐ์ค€์„ ์„ 1์œ„ ์ž๋ฆฌ์— ์•‰ํžˆ์ง€ ์•Š๋Š”๋‹ค.
221
- ๊ธฐ์ค€์„ ์€ ๋„˜์–ด์•ผ ํ•  ์„ ์ด์ง€ ์šฐ์Šน์ž๊ฐ€ ์•„๋‹ˆ๋‹ค.
222
- """
223
- def build():
224
- out = []
225
- for name, b in boards().items():
226
- rolled = ledger_read("leaderboard/%s.json" % name.lower(), {}) or {}
227
- ent = (rolled.get("entries") or [])
228
- top = ent[0] if ent else None
229
- nf = (b.get("noise_floor") or {}).get("sd_single")
230
- base = b.get("baselines") or {}
231
- row = {"board": name, "n_test": b.get("n_test"),
232
- "split_grade": b.get("split_grade"), "answer_grade": b.get("answer_grade"),
233
- "noise_floor": nf, "entries": len(ent),
234
- "baseline_best": (min((v["mae"] for v in base.values() if v.get("mae") is not None),
235
- default=None)),
236
- "baseline_best_auroc": (max((v["auroc"] for v in base.values()
237
- if v.get("auroc") is not None), default=None)),
238
- "leader": None}
239
- if top:
240
- row["leader"] = {"method": top.get("method"), "user": top.get("user"),
241
- "mae": top.get("mae"), "auroc": top.get("auroc"),
242
- "verified": bool(top.get("verified")), "leak": top.get("leak")}
243
- out.append(row)
244
- out.sort(key=lambda r: (r["leader"] is None, -(r["n_test"] or 0)))
245
- return out
246
- rows = cached("leaders", 60, build)
247
- return {"spec": SPEC, "n": len(rows), "rows": rows,
248
- "held": sum(1 for r in rows if r["leader"])}
249
-
250
-
251
- @app.get("/api/board/{name}")
252
- def api_board(name: str):
253
- b = boards().get(name)
254
- if not b:
255
- raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
256
- return b
257
-
258
-
259
- @app.get("/api/board/{name}/testset")
260
- def api_testset(name: str):
261
- """ํ…Œ์ŠคํŠธ์…‹. ๊ตฌ์กฐ๋งŒ ๋‚˜๊ฐ„๋‹ค - ๋ผ๋ฒจ์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค."""
262
- p = os.path.join(DATA, "%s_test.json" % name.lower())
263
- if not os.path.exists(p):
264
- raise HTTPException(404, "ํ…Œ์ŠคํŠธ์…‹์ด ์•„์ง ์—†๋‹ค")
265
- return FileResponse(p, media_type="application/json",
266
- filename="%s_test.json" % name.lower())
267
-
268
-
269
- @app.get("/api/board/{name}/leaderboard")
270
- def api_leaderboard(name: str):
271
- """์ˆœ์œ„ํ‘œ. ๊ธฐ์ค€์„ ์€ ํ•ญ์ƒ ํฌํ•จ๋œ๋‹ค (3์กฐ).
272
-
273
- ์ฐธ๊ฐ€์ž ํ•ญ๋ชฉ์€ ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ๊ฒƒ์„ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ค€๋‹ค. ์—ฌ๊ธฐ์„œ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๋Š”๋‹ค -
274
- ๊ณ„์‚ฐํ•˜๋ ค๋ฉด ์ •๋‹ต์ด ์žˆ์–ด์•ผ ํ•˜๊ณ , ์ •๋‹ต์€ ์—ฌ๊ธฐ ์—†๋‹ค.
275
- """
276
- b = boards().get(name)
277
- if not b:
278
- raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
279
- rolled = cached("lb:" + name, 60,
280
- lambda: ledger_read("leaderboard/%s.json" % name.lower(),
281
- {"entries": [], "updated": None})) or {}
282
- rows = []
283
- for k, v in (b.get("baselines") or {}).items():
284
- rows.append({"method": k, "user": "โ€”", "kind": "baseline",
285
- "mae": v["mae"], "auroc": v["auroc"], "prauc": v["prauc"]})
286
- for e in rolled.get("entries", []):
287
- rows.append(dict(e, kind=e.get("kind", "entry")))
288
- rows.sort(key=lambda r: r.get("mae", 9e9))
289
- nf = (b.get("noise_floor") or {}).get("sd_single")
290
- best = rows[0]["mae"] if rows else None
291
- # 4์กฐ: ์ตœ๊ณ ์ ์—์„œ ์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ๊ฐ™์€ ๊ณ„๋‹จ์œผ๋กœ ๋ฌถ๋Š”๋‹ค.
292
- for r in rows:
293
- r["within_noise"] = bool(nf and best is not None and r["mae"] - best < nf)
294
- rank = 0
295
- for r in rows:
296
- if r["kind"] == "baseline":
297
- r["rank"] = None
298
- else:
299
- rank += 1
300
- r["rank"] = rank
301
- return {"board": name, "noise_floor": nf, "rows": rows,
302
- "updated": rolled.get("updated"),
303
- "note": "์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค (4์กฐ)"}
304
-
305
-
306
- # ------------------------------------------------------------------ ๋กœ๊ทธ์ธ
307
- @app.get("/login")
308
- def login(request: Request):
309
- if not OAUTH_ID:
310
- raise HTTPException(503, "๋กœ๊ทธ์ธ์ด ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค")
311
- nxt = request.query_params.get("next", "/")
312
- st = base64.urlsafe_b64encode(json.dumps({"n": nxt, "r": secrets.token_hex(8)}).encode()).decode()
313
- q = urllib.parse.urlencode({
314
- "client_id": OAUTH_ID, "redirect_uri": _redirect(request),
315
- "response_type": "code", "scope": "openid profile", "state": st})
316
- return RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS, q))
317
-
318
-
319
- def _redirect(request: Request):
320
- if SPACE_HOST:
321
- return "https://%s/auth" % SPACE_HOST
322
- return str(request.base_url).rstrip("/") + "/auth"
323
-
324
-
325
- @app.get("/auth")
326
- def auth(request: Request):
327
- code = request.query_params.get("code")
328
- st = request.query_params.get("state") or ""
329
- nxt = "/"
330
- try:
331
- nxt = json.loads(base64.urlsafe_b64decode(st.encode()).decode()).get("n", "/")
332
- except Exception:
333
- pass
334
- if not code:
335
- return RedirectResponse(nxt)
336
- body = urllib.parse.urlencode({
337
- "client_id": OAUTH_ID, "client_secret": OAUTH_SECRET,
338
- "grant_type": "authorization_code", "code": code,
339
- "redirect_uri": _redirect(request)}).encode()
340
- with urllib.request.urlopen(urllib.request.Request(
341
- OAUTH_ISS + "/oauth/token", data=body,
342
- headers={"Content-Type": "application/x-www-form-urlencoded"}), timeout=60) as r:
343
- tok = json.loads(r.read())
344
- with urllib.request.urlopen(urllib.request.Request(
345
- OAUTH_ISS + "/oauth/userinfo",
346
- headers={"Authorization": "Bearer " + tok["access_token"]}), timeout=60) as r:
347
- ui = json.loads(r.read())
348
- resp = RedirectResponse(nxt)
349
- set_session(resp, {"user": ui.get("preferred_username") or ui.get("sub"),
350
- "name": ui.get("name", ""), "pic": ui.get("picture", "")})
351
- return resp
352
-
353
-
354
- @app.get("/logout")
355
- def logout():
356
- r = RedirectResponse("/")
357
- r.delete_cookie(COOKIE, **COOKIE_KW)
358
- return r
359
-
360
-
361
- @app.get("/api/me")
362
- def api_me(request: Request):
363
- u = who(request)
364
- return {"user": u, "login_enabled": bool(OAUTH_ID), "daily_cap": DAILY_CAP}
365
-
366
-
367
- # ------------------------------------------------------------------ ์ œ์ถœ
368
- class Submission(BaseModel):
369
- board: str
370
- method: str # ๋ฐฉ๋ฒ• ์ด๋ฆ„. ์ˆœ์œ„ํ‘œ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋œ๋‹ค
371
- predictions: dict # {compound_id: ์˜ˆ์ธก๊ฐ’}
372
- training_data: str = "" # 5์กฐ ๋ˆ„์ถœ๊ฒ€์‚ฌ์— ์“ด๋‹ค
373
- pretrained_on: str = ""
374
- code_url: str = ""
375
- container: str = "" # ์žˆ์œผ๋ฉด [๊ฒ€์ฆ๋จ] ์‹ฌ์‚ฌ ๋Œ€์ƒ (6์กฐ)
376
-
377
-
378
- @app.post("/api/submit")
379
- def submit(s: Submission, request: Request):
380
- u = who(request)
381
- if not u:
382
- raise HTTPException(401, "์ œ์ถœํ•˜๋ ค๋ฉด ๋กœ๊ทธ์ธํ•ด์•ผ ํ•œ๋‹ค")
383
- if not HF_TOKEN:
384
- raise HTTPException(503, "์›์žฅ์ด ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค")
385
- b = boards().get(s.board)
386
- if not b:
387
- raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
388
- if not re.fullmatch(r"[\w .\-+/()]{2,60}", s.method or ""):
389
- raise HTTPException(400, "๋ฐฉ๋ฒ• ์ด๋ฆ„์€ 2~60์ž์—ฌ์•ผ ํ•œ๋‹ค")
390
- # ํ•˜๋ฃจ ํ•œ๋„. 8์กฐ๊ฐ€ ์ ์ˆ˜ ๊ณต๊ฐœ๋ฅผ ๋ง‰์•„๋„ ์ œ์ถœ ์ž์ฒด๋Š” ๋น„์šฉ์ด ๋“ค๊ณ , ๋ฌด์ œํ•œ์ด๋ฉด
391
- # ์›์žฅ์ด ์ž ๊ธด๋‹ค. ๊ฒฝ๊ณ„๋Š” ์ฐธ๊ฐ€์ž๊ฐ€ ๊ฐ€์ •ํ•  ํ•˜๋ฃจ(KST)๋กœ ์žก๋Š”๋‹ค.
392
- day = int((time.time() + 9 * 3600) // 86400)
393
- key = "cap:%s:%d" % (u["user"], day)
394
- used = _CAP.get(key, 0)
395
- if used >= DAILY_CAP:
396
- raise HTTPException(429, "์˜ค๋Š˜ ์ œ์ถœ ํ•œ๋„ %dํšŒ๋ฅผ ๋‹ค ์ผ๋‹ค. ํ•œ๊ตญ์‹œ๊ฐ„ ์ž์ •์— ์ดˆ๊ธฐํ™”๋œ๋‹ค"
397
- % DAILY_CAP)
398
-
399
- n = b.get("n_test", 0)
400
- got = len(s.predictions or {})
401
- if got < n:
402
- raise HTTPException(400, "์˜ˆ์ธก์ด %d๊ฐœ ํ•„์š”ํ•œ๋ฐ %d๊ฐœ๋‹ค. ๋นˆ ํ•ญ๋ชฉ์€ ์ฑ„์›Œ ๋ณด๋‚ด๋ผ "
403
- "- ์ž„์˜๋กœ ๋ฉ”์šฐ๋ฉด ๊ทธ ๊ฐ’์ด ์ ์ˆ˜์— ๋“ค์–ด๊ฐ„๋‹ค" % (n, got))
404
- sid = hashlib.sha256(("%s|%s|%s|%f" % (s.board, u["user"], s.method, time.time()))
405
- .encode()).hexdigest()[:16].upper()
406
- rec = {"submission_id": sid, "board": s.board, "hf_user": u["user"],
407
- "method": s.method, "predictions": s.predictions,
408
- "training_data": s.training_data, "pretrained_on": s.pretrained_on,
409
- "code_url": s.code_url, "container": s.container,
410
- "submitted_at": int(time.time()), "spec": SPEC,
411
- "dataset_sha": b.get("dataset_sha")}
412
- ledger_write("submissions/%s/%s.json" % (s.board.lower(), sid), rec,
413
- "submit %s %s" % (s.board, sid))
414
- _CAP[key] = used + 1
415
- return {"ok": True, "submission_id": sid,
416
- "remaining_today": DAILY_CAP - _CAP[key],
417
- "note": "์ฑ„์ ์€ ์›Œ์ปค๊ฐ€ ํ•œ๋‹ค. 8์กฐ์— ๋”ฐ๋ผ ์ง์ „ ์ตœ๊ณ ์ ์„ ์žก์Œ ๋ฐ”๋‹ฅ ์ด์ƒ์œผ๋กœ "
418
- "๋„˜์—ˆ์„ ๋•Œ๋งŒ ์ƒˆ ์ ์ˆ˜๊ฐ€ ๊ณต๊ฐœ๋œ๋‹ค."}
419
-
420
-
421
- @app.get("/api/health")
422
- def health():
423
- return {"ok": True, "spec": SPEC, "boards": len(boards()),
424
- "ledger": bool(HF_TOKEN), "login": bool(OAUTH_ID),
425
- "scoring_here": False}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """LEADBOARD โ€” ์‹ ์•ฝ ์˜ˆ์ธก ๋„๊ตฌ๋ฅผ ๋ถ„์•ผ๋ณ„๋กœ ๊ฐ™์€ ์žฃ๋Œ€์— ์„ธ์šฐ๋Š” ์‹œํ—˜๋Œ€.
3
+
4
+ **์ด ์„œ๋น„์Šค๊ฐ€ ํ•˜๋Š” ์ผ**
5
+ ยท ์นดํ…Œ๊ณ ๋ฆฌยท๋ถ€๋ฌธ ์นด๋“œ๋ฅผ ์„œ๋น™ํ•œ๋‹ค
6
+ ยท ํ…Œ์ŠคํŠธ์…‹(๊ตฌ์กฐ๋งŒ)์„ ๋‚ด๋ ค์ค€๋‹ค
7
+ ยท ์ œ์ถœ์„ ๋ฐ›์•„ ๋น„๊ณต๊ฐœ ์›์žฅ์— ์ ๋Š”๋‹ค
8
+ ยท ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ์ˆœ์œ„ํ‘œ๋ฅผ ๋ณด์—ฌ์ค€๋‹ค
9
+
10
+ **ํ•˜์ง€ ์•Š๋Š” ์ผ: ์ฑ„์ .**
11
+ ์ •๋‹ต์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์—†๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์ด๋ฏธ์ง€๋Š” ๋ˆ„๊ตฌ๋‚˜ ๋ฐ›์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, ์—ฌ๊ธฐ์— ๋ผ๋ฒจ์„ ๋‘๋ฉด
12
+ ๊ทธ ์ˆœ๊ฐ„ 1์กฐ๊ฐ€ ๋ฌด๋„ˆ์ง„๋‹ค. ์ฑ„์ ์€ ์›์žฅ์„ ํด๋งํ•˜๋Š” ๋ณ„๋„ ์›Œ์ปค๊ฐ€ ๋กœ์ปฌ ์ •๋‹ต ํŒŒ์ผ๋กœ ํ•œ๋‹ค.
13
+
14
+ ๋ถ€๋ฌธ ์นด๋“œ์—๋Š” **์ฑ„์ ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ณต๊ฐœ ์ •๋ณด**๊ฐ€ ๋“ค์–ด ์žˆ๋‹ค โ€”
15
+ ๋ถ„ํ•  ๋“ฑ๊ธ‰ ยท ์ •๋‹ต ๋“ฑ๊ธ‰ ยท ์žก์Œ ๋ฐ”๋‹ฅ ยท ๊ธฐ์ค€์„  ์„ฑ์  ยท ๋ฐ์ดํ„ฐ ์ง€๋ฌธ.
16
+ ์šฐ๋ฆฌ ์ ์ˆ˜๋ฅผ ๋ฏฟ์–ด๋‹ฌ๋ผ๊ณ  ํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด์„œ๋‹ค.
17
+ """
18
+ import base64
19
+ import glob
20
+ import hashlib
21
+ import hmac
22
+ import io
23
+ import json
24
+ import os
25
+ import re
26
+ import secrets
27
+ import time
28
+ import urllib.parse
29
+ import urllib.request
30
+
31
+ from fastapi import FastAPI, HTTPException, Request
32
+ from fastapi.middleware.gzip import GZipMiddleware
33
+ from fastapi.responses import (FileResponse, JSONResponse, RedirectResponse,
34
+ Response)
35
+ from pydantic import BaseModel
36
+
37
+ HERE = os.path.dirname(os.path.abspath(__file__))
38
+ DATA = os.path.join(HERE, "data")
39
+ SPEC = "v1.1"
40
+
41
+ LEDGER = os.environ.get("LB_LEDGER_REPO", "FINAL-Bench/leadboard-submissions")
42
+ LEDGER_API = "https://huggingface.co/api/datasets/%s" % LEDGER
43
+ LEDGER_RAW = "https://huggingface.co/datasets/%s/resolve/main" % LEDGER
44
+ HF_TOKEN = os.environ.get("HF_TOKEN", "")
45
+
46
+ OAUTH_ID = os.environ.get("OAUTH_CLIENT_ID", "")
47
+ OAUTH_SECRET = os.environ.get("OAUTH_CLIENT_SECRET", "")
48
+ OAUTH_ISS = os.environ.get("OPENID_PROVIDER_URL", "https://huggingface.co")
49
+ SPACE_HOST = os.environ.get("SPACE_HOST", "")
50
+ COOKIE = "lb_session"
51
+ IN_FRAME = bool(SPACE_HOST)
52
+ COOKIE_KW = ({"samesite": "none", "secure": True} if IN_FRAME
53
+ else {"samesite": "lax", "secure": False})
54
+ SESSION_KEY = os.environ.get("LB_SESSION_KEY") or secrets.token_hex(16)
55
+ DAILY_CAP = int(os.environ.get("LB_DAILY_CAP", "5"))
56
+
57
+ app = FastAPI(title="LEADBOARD")
58
+ app.add_middleware(GZipMiddleware, minimum_size=1024)
59
+
60
+ _C = {}
61
+ # ํ•˜๋ฃจ ํ•œ๋„ ์นด์šดํ„ฐ. ์ปจํ…Œ์ด๋„ˆ ์žฌ๊ธฐ๋™์ด๋ฉด ๋น„๋Š”๋ฐ, ๊ทธ๋ž˜๋„ ํญ๏ฟฝ๏ฟฝ๏ฟฝ๋Š” ๋ง‰๋Š”๋‹ค.
62
+ # ์ •ํ™•ํ•œ ํšŒ๊ณ„๊ฐ€ ํ•„์š”ํ•ด์ง€๋ฉด ์›์žฅ ์ชฝ์œผ๋กœ ์˜ฎ๊ธด๋‹ค.
63
+ _CAP = {}
64
+
65
+
66
+ def cached(key, ttl, produce):
67
+ hit = _C.get(key)
68
+ if hit and time.time() - hit[0] < ttl:
69
+ return hit[1]
70
+ try:
71
+ v = produce()
72
+ except Exception:
73
+ if hit:
74
+ return hit[1]
75
+ raise
76
+ _C[key] = (time.time(), v)
77
+ return v
78
+
79
+
80
+ # ------------------------------------------------------------------ ๋ถ€๋ฌธ ์นด๋“œ
81
+ def _sha(path):
82
+ h = hashlib.sha256()
83
+ with open(path, "rb") as f:
84
+ for b in iter(lambda: f.read(65536), b""):
85
+ h.update(b)
86
+ return h.hexdigest()[:16]
87
+
88
+
89
+ def _load_boards():
90
+ out = {}
91
+ for p in sorted(glob.glob(os.path.join(DATA, "*_card.json"))):
92
+ try:
93
+ d = json.load(io.open(p, encoding="utf-8"))
94
+ except Exception:
95
+ continue
96
+ nf = d.get("noise_floor") or {}
97
+ base = d.get("baselines") or {}
98
+ # ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—๋Š” MAE ๊ธฐ์ค€์„ ์ด ์—†๋‹ค. ๊ทธ๋Ÿด ๋•Œ๋Š” AUROC ๊ฐ€ ๊ฐ€์žฅ ๋†’์€ ๊ฒƒ์„ ์ตœ์„ ์œผ๋กœ ๋ณธ๋‹ค.
99
+ # ์—†๋Š” ๊ฐ’์„ 0 ์œผ๋กœ ์น˜๋ฉด ๊ทธ ๋ถ€๋ฌธ์ด "๊ฐ€์žฅ ์ •ํ™•ํ•œ ๊ณณ"์œผ๋กœ ํ‘œ์— ์˜ค๋ฅธ๋‹ค.
100
+ with_mae = {k: v for k, v in base.items() if v.get("mae") is not None}
101
+ if with_mae:
102
+ best = min(with_mae, key=lambda k: with_mae[k]["mae"])
103
+ d["metric"] = "mae"
104
+ elif base:
105
+ best = max(base, key=lambda k: base[k].get("auroc") or 0)
106
+ d["metric"] = "auroc"
107
+ else:
108
+ best = None
109
+ d["metric"] = None
110
+ d["dataset_sha"] = _sha(p)
111
+ d["spec"] = SPEC
112
+ d["best_baseline"] = best
113
+ # ๋ชจ๋ธ ์˜ค์ฐจ๊ฐ€ ์‹คํ—˜ ์˜ค์ฐจ์˜ ๋ช‡ ๋ฐฐ์ธ๊ฐ€. 1 ์— ๋ถ™์„์ˆ˜๋ก ์ธก์ • ํ•œ๊ณ„๋‹ค.
114
+ if best and nf.get("sd_single") and d["metric"] == "mae":
115
+ d["error_over_noise"] = round(base[best]["mae"] / nf["sd_single"], 2)
116
+ if ("์ƒ์ˆ˜ ์˜ˆ์ธก" in base and "Morgan+LightGBM" in base
117
+ and base["Morgan+LightGBM"].get("mae") is not None
118
+ and base["์ƒ์ˆ˜ ์˜ˆ์ธก"].get("mae") is not None):
119
+ d["beats_constant"] = base["Morgan+LightGBM"]["mae"] < base["์ƒ์ˆ˜ ์˜ˆ์ธก"]["mae"]
120
+ if d.get("n_test"):
121
+ d["near_pct"] = round(100.0 * d.get("near_threshold", 0) / d["n_test"], 1)
122
+ d["open"] = True
123
+ out[d["board"]] = d
124
+ return out
125
+
126
+
127
+ def boards():
128
+ return cached("boards", 300, _load_boards)
129
+
130
+
131
+ def _load_references():
132
+ """ํ˜„์—… ๋„๊ตฌ๋ฅผ ์ด ๋ถ€๋ฌธ ํ›ˆ๋ จ ์ž๋ฃŒ๋กœ ์žฌํ•™์Šตํ•ด ์–ป์€ ์„ฑ์ .
133
+
134
+ ๊ธฐ์กด ๋ชจ๋ธ์„ ๊ทธ๋Œ€๋กœ ์˜ฎ๊ฒจ ์žฌ๋ฉด ํ›ˆ๋ จ ์ž๋ฃŒ์˜ ์ฐจ์ด๊ฐ€ ๋ฐฉ๋ฒ•์˜ ์ฐจ์ด๋กœ ์ฝํžŒ๋‹ค. ์‹ค์ œ๋กœ ๊ทธ๋ ‡๊ฒŒ
135
+ ๋‚˜์™”์—ˆ๋‹ค - 523 ํ™”ํ•ฉ๋ฌผ๋กœ ํ•™์Šตํ•œ ๋ชจ๋ธ์ด 8,450 ํ™”ํ•ฉ๋ฌผ๋กœ ํ•™์Šตํ•œ ๋ฌดํŠœ๋‹ ๊ธฐ์ค€์„ ์— ์กŒ๋‹ค.
136
+ ๊ทธ๋ž˜์„œ **๋ฐฉ๋ฒ•๋งŒ ๊ฐ€์ ธ์˜ค๊ณ  ๋ฐ์ดํ„ฐ๋Š” ์ด ๋ถ€๋ฌธ ๊ฒƒ์„ ์“ด๋‹ค.**
137
+ """
138
+ p = os.path.join(DATA, "reference_entries.json")
139
+ if not os.path.exists(p):
140
+ return {}
141
+ try:
142
+ d = json.load(io.open(p, encoding="utf-8"))
143
+ except Exception:
144
+ return {}
145
+ out = {}
146
+ for e in d.get("entries", []):
147
+ out.setdefault(e["board"], []).append(dict(e, method=d.get("method", "reference")))
148
+ return out
149
+
150
+
151
+ def references():
152
+ return cached("refs", 300, _load_references)
153
+
154
+
155
+ def categories():
156
+ def build():
157
+ cats = json.load(io.open(os.path.join(HERE, "categories.json"), encoding="utf-8"))
158
+ bd = boards()
159
+ for c in cats["categories"]:
160
+ c["open"] = len([b for b in c["boards"] if b in bd])
161
+ c["board_cards"] = [bd[b] for b in c["boards"] if b in bd]
162
+ return cats
163
+ return cached("cats", 300, build)
164
+
165
+
166
+ # ------------------------------------------------------------------ ์›์žฅ
167
+ def _hdr():
168
+ return {"Authorization": "Bearer " + HF_TOKEN, "User-Agent": "LEADBOARD/1.0"}
169
+
170
+
171
+ def ledger_read(path, default=None):
172
+ try:
173
+ with urllib.request.urlopen(urllib.request.Request(
174
+ "%s/%s" % (LEDGER_RAW, path), headers=_hdr()), timeout=60) as r:
175
+ return json.loads(r.read())
176
+ except Exception:
177
+ return default
178
+
179
+
180
+ def ledger_write(path, obj, summary):
181
+ blob = base64.b64encode(json.dumps(obj, ensure_ascii=False).encode()).decode()
182
+ lines = [json.dumps({"key": "header", "value": {"summary": summary}}),
183
+ json.dumps({"key": "file", "value": {"path": path, "content": blob,
184
+ "encoding": "base64"}})]
185
+ req = urllib.request.Request(LEDGER_API + "/commit/main",
186
+ data=("\n".join(lines) + "\n").encode(),
187
+ headers=dict(_hdr(), **{"Content-Type": "application/x-ndjson"}))
188
+ with urllib.request.urlopen(req, timeout=180) as r:
189
+ return json.loads(r.read())
190
+
191
+
192
+ # ------------------------------------------------------------------ ์„ธ์…˜
193
+ def sign(v):
194
+ return hmac.new(SESSION_KEY.encode(), v.encode(), hashlib.sha256).hexdigest()[:32]
195
+
196
+
197
+ def set_session(resp, user):
198
+ raw = json.dumps(user, ensure_ascii=False)
199
+ b = base64.urlsafe_b64encode(raw.encode()).decode()
200
+ resp.set_cookie(COOKIE, "%s.%s" % (b, sign(b)), max_age=86400 * 7,
201
+ httponly=True, **COOKIE_KW)
202
+
203
+
204
+ def who(req: Request):
205
+ c = req.cookies.get(COOKIE) or ""
206
+ if "." not in c:
207
+ return None
208
+ b, sg = c.rsplit(".", 1)
209
+ if not hmac.compare_digest(sg, sign(b)):
210
+ return None
211
+ try:
212
+ return json.loads(base64.urlsafe_b64decode(b.encode()).decode())
213
+ except Exception:
214
+ return None
215
+
216
+
217
+ # ------------------------------------------------------------------ ๋ผ์šฐํŠธ
218
+ @app.get("/")
219
+ def index():
220
+ return FileResponse(os.path.join(HERE, "index.html"))
221
+
222
+
223
+ @app.get("/i18n.js")
224
+ def i18n():
225
+ """๋ฌธ์ž์—ด ์‚ฌ์ „. ํ™”๋ฉด ์ฝ”๋“œ์™€ ๋ถ„๋ฆฌํ•ด ๋‘์–ด์•ผ ์˜์–ดํŒ์ด ์กฐ์šฉํžˆ ๋’ค์ฒ˜์ง€์ง€ ์•Š๋Š”๋‹ค."""
226
+ return FileResponse(os.path.join(HERE, "i18n.js"),
227
+ media_type="application/javascript")
228
+
229
+
230
+ @app.get("/api/categories")
231
+ def api_categories():
232
+ c = categories()
233
+ bd = boards()
234
+ return {"spec": SPEC, "categories": c["categories"],
235
+ "totals": {"planned": sum(x["planned"] for x in c["categories"]),
236
+ "open": len(bd), "ledger": bool(HF_TOKEN)}}
237
+
238
+
239
+ @app.get("/api/leaders")
240
+ def api_leaders():
241
+ """๋ถ€๋ฌธ๋งˆ๋‹ค ํ˜„์žฌ 1์œ„ ํ•œ ์ค„. ์ฒซ ํ™”๋ฉด์—์„œ ์ „์ฒด๋ฅผ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ์œ„ํ•œ ๊ฒƒ์ด๋‹ค.
242
+
243
+ ๋ถ€๋ฌธ๋ณ„๋กœ ์ˆœ์œ„ํ‘œ๋ฅผ ๋”ฐ๋กœ ๋ถ€๋ฅด๋ฉด ์™•๋ณต์ด ๋ถ€๋ฌธ ์ˆ˜๋งŒํผ ๋Š˜์–ด๋‚œ๋‹ค. ์—ฌ๊ธฐ์„œ ํ•œ ๋ฒˆ์— ๋ชจ์€๋‹ค.
244
+ **์ฐธ๊ฐ€ ์ œ์ถœ์ด ์—†์œผ๋ฉด ๋น„์›Œ์„œ ๋ณด๋‚ธ๋‹ค** - ๊ธฐ์ค€์„ ์„ 1์œ„ ์ž๋ฆฌ์— ์•‰ํžˆ์ง€ ์•Š๋Š”๋‹ค.
245
+ ๊ธฐ์ค€์„ ์€ ๋„˜์–ด์•ผ ํ•  ์„ ์ด์ง€ ์šฐ์Šน์ž๊ฐ€ ์•„๋‹ˆ๋‹ค.
246
+ """
247
+ def build():
248
+ out = []
249
+ for name, b in boards().items():
250
+ rolled = ledger_read("leaderboard/%s.json" % name.lower(), {}) or {}
251
+ ent = (rolled.get("entries") or [])
252
+ top = ent[0] if ent else None
253
+ nf = (b.get("noise_floor") or {}).get("sd_single")
254
+ base = b.get("baselines") or {}
255
+ row = {"board": name, "n_test": b.get("n_test"),
256
+ "split_grade": b.get("split_grade"), "answer_grade": b.get("answer_grade"),
257
+ "noise_floor": nf, "entries": len(ent),
258
+ "baseline_best": (min((v["mae"] for v in base.values() if v.get("mae") is not None),
259
+ default=None)),
260
+ "baseline_best_auroc": (max((v["auroc"] for v in base.values()
261
+ if v.get("auroc") is not None), default=None)),
262
+ "leader": None}
263
+ if top:
264
+ row["leader"] = {"method": top.get("method"), "user": top.get("user"),
265
+ "mae": top.get("mae"), "auroc": top.get("auroc"),
266
+ "verified": bool(top.get("verified")), "leak": top.get("leak")}
267
+ out.append(row)
268
+ out.sort(key=lambda r: (r["leader"] is None, -(r["n_test"] or 0)))
269
+ return out
270
+ rows = cached("leaders", 60, build)
271
+ return {"spec": SPEC, "n": len(rows), "rows": rows,
272
+ "held": sum(1 for r in rows if r["leader"])}
273
+
274
+
275
+ @app.get("/api/board/{name}")
276
+ def api_board(name: str):
277
+ b = boards().get(name)
278
+ if not b:
279
+ raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
280
+ return b
281
+
282
+
283
+ @app.get("/api/board/{name}/testset")
284
+ def api_testset(name: str):
285
+ """ํ…Œ์ŠคํŠธ์…‹. ๊ตฌ์กฐ๋งŒ ๋‚˜๊ฐ„๋‹ค - ๋ผ๋ฒจ์€ ์ด ์ปจํ…Œ์ด๋„ˆ์— ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค."""
286
+ p = os.path.join(DATA, "%s_test.json" % name.lower())
287
+ if not os.path.exists(p):
288
+ raise HTTPException(404, "ํ…Œ์ŠคํŠธ์…‹์ด ์•„์ง ์—†๋‹ค")
289
+ return FileResponse(p, media_type="application/json",
290
+ filename="%s_test.json" % name.lower())
291
+
292
+
293
+ @app.get("/api/board/{name}/leaderboard")
294
+ def api_leaderboard(name: str):
295
+ """์ˆœ์œ„ํ‘œ. ๊ธฐ์ค€์„ ์€ ํ•ญ์ƒ ํฌํ•จ๋œ๋‹ค (3์กฐ).
296
+
297
+ ์ฐธ๊ฐ€์ž ํ•ญ๋ชฉ์€ ์›Œ์ปค๊ฐ€ ๊ตด๋ ค ๋†“์€ ๊ฒƒ์„ ๊ทธ๋Œ€๋กœ ๋ณด์—ฌ์ค€๋‹ค. ์—ฌ๊ธฐ์„œ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๋Š”๋‹ค -
298
+ ๊ณ„์‚ฐํ•˜๋ ค๋ฉด ์ •๋‹ต์ด ์žˆ์–ด์•ผ ํ•˜๊ณ , ์ •๋‹ต์€ ์—ฌ๊ธฐ ์—†๋‹ค.
299
+ """
300
+ b = boards().get(name)
301
+ if not b:
302
+ raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
303
+ rolled = cached("lb:" + name, 60,
304
+ lambda: ledger_read("leaderboard/%s.json" % name.lower(),
305
+ {"entries": [], "updated": None})) or {}
306
+ rows = []
307
+ for k, v in (b.get("baselines") or {}).items():
308
+ rows.append({"method": k, "user": "โ€”", "kind": "baseline",
309
+ "mae": v["mae"], "auroc": v["auroc"], "prauc": v["prauc"]})
310
+ # ํ˜„์—… ๋„๊ตฌ ์ฐธ์กฐ ํ•ญ๋ชฉ. ๊ธฐ์ค€์„ ๊ณผ ์ฐธ๊ฐ€ ์ œ์ถœ ์‚ฌ์ด์— ๋†“๋Š”๋‹ค - ํ•™์Šตํ•˜์ง€ ์•Š์€ ์„ ๋„ ์•„๋‹ˆ๊ณ 
311
+ # ์ด๋ฒˆ ํšŒ์ฐจ์˜ ์ฐธ๊ฐ€์ž๋„ ์•„๋‹ˆ๋‹ค. ์ฐธ๊ฐ€์ž๊ฐ€ ์ž๊ธฐ ์œ„์น˜๋ฅผ ๊ฐ€๋Š ํ•  ์„ธ ๋ฒˆ์งธ ์ขŒํ‘œ๋‹ค.
312
+ for r in references().get(name, []):
313
+ rows.append({"method": r["method"], "user": "โ€”", "kind": "reference",
314
+ "mae": r.get("mae"), "auroc": r.get("auroc"),
315
+ "prauc": r.get("prauc"), "leak": r.get("leak")})
316
+ for e in rolled.get("entries", []):
317
+ rows.append(dict(e, kind=e.get("kind", "entry")))
318
+ # ๋ถ€๋ฌธ ์ฃผ์ง€ํ‘œ๋กœ ์„ธ์šด๋‹ค. ๋ถ„๋ฅ˜ ์ „์šฉ ๋ถ€๋ฌธ์—์„œ mae ๋กœ ์„ธ์šฐ๋ฉด ์ „๋ถ€ ๋™๋ฅ ์ด ๋œ๋‹ค.
319
+ if b.get("metric") == "auroc":
320
+ rows.sort(key=lambda r: -(r.get("auroc") or 0))
321
+ else:
322
+ rows.sort(key=lambda r: (r.get("mae") is None, r.get("mae") or 9e9))
323
+ nf = (b.get("noise_floor") or {}).get("sd_single")
324
+ best = rows[0].get("mae") if rows else None
325
+ # 4์กฐ: ์ตœ๊ณ ์ ์—์„œ ์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ๊ฐ™์€ ๊ณ„๋‹จ์œผ๋กœ ๋ฌถ๋Š”๋‹ค.
326
+ for r in rows:
327
+ r["within_noise"] = bool(nf and best is not None and r.get("mae") is not None
328
+ and r["mae"] - best < nf)
329
+ rank = 0
330
+ for r in rows:
331
+ # ์ˆœ์œ„๋Š” ์ฐธ๊ฐ€ ์ œ์ถœ์—๋งŒ ๋งค๊ธด๋‹ค. ๊ธฐ์ค€์„ ๊ณผ ์ฐธ์กฐ ๋„๊ตฌ๋Š” ํ‘œ์— ์„œ๋˜ ๋“ฑ์ˆ˜๋ฅผ ๊ฐ–์ง€ ์•Š๋Š”๋‹ค -
332
+ # ์šฐ๋ฆฌ๊ฐ€ ์˜ฌ๋ฆฐ ๊ฒƒ์ด 1์œ„ ์ž๋ฆฌ๋ฅผ ์ฐจ์ง€ํ•˜๋ฉด ์ฐธ๊ฐ€์ž์—๊ฒŒ ๊ฒจ๋ฃฐ ์ž๋ฆฌ๊ฐ€ ์—†๋‹ค.
333
+ if r["kind"] in ("baseline", "reference"):
334
+ r["rank"] = None
335
+ else:
336
+ rank += 1
337
+ r["rank"] = rank
338
+ return {"board": name, "noise_floor": nf, "rows": rows,
339
+ "updated": rolled.get("updated"),
340
+ "note": "์žก์Œ ๋ฐ”๋‹ฅ ์•ˆ์— ๋“  ํ•ญ๋ชฉ์€ ์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค (4์กฐ)"}
341
+
342
+
343
+ # ------------------------------------------------------------------ ๋กœ๊ทธ์ธ
344
+ @app.get("/login")
345
+ def login(request: Request):
346
+ if not OAUTH_ID:
347
+ raise HTTPException(503, "๋กœ๊ทธ์ธ์ด ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค")
348
+ nxt = request.query_params.get("next", "/")
349
+ st = base64.urlsafe_b64encode(json.dumps({"n": nxt, "r": secrets.token_hex(8)}).encode()).decode()
350
+ q = urllib.parse.urlencode({
351
+ "client_id": OAUTH_ID, "redirect_uri": _redirect(request),
352
+ "response_type": "code", "scope": "openid profile", "state": st})
353
+ return RedirectResponse("%s/oauth/authorize?%s" % (OAUTH_ISS, q))
354
+
355
+
356
+ def _redirect(request: Request):
357
+ if SPACE_HOST:
358
+ return "https://%s/auth" % SPACE_HOST
359
+ return str(request.base_url).rstrip("/") + "/auth"
360
+
361
+
362
+ @app.get("/auth")
363
+ def auth(request: Request):
364
+ code = request.query_params.get("code")
365
+ st = request.query_params.get("state") or ""
366
+ nxt = "/"
367
+ try:
368
+ nxt = json.loads(base64.urlsafe_b64decode(st.encode()).decode()).get("n", "/")
369
+ except Exception:
370
+ pass
371
+ if not code:
372
+ return RedirectResponse(nxt)
373
+ body = urllib.parse.urlencode({
374
+ "client_id": OAUTH_ID, "client_secret": OAUTH_SECRET,
375
+ "grant_type": "authorization_code", "code": code,
376
+ "redirect_uri": _redirect(request)}).encode()
377
+ with urllib.request.urlopen(urllib.request.Request(
378
+ OAUTH_ISS + "/oauth/token", data=body,
379
+ headers={"Content-Type": "application/x-www-form-urlencoded"}), timeout=60) as r:
380
+ tok = json.loads(r.read())
381
+ with urllib.request.urlopen(urllib.request.Request(
382
+ OAUTH_ISS + "/oauth/userinfo",
383
+ headers={"Authorization": "Bearer " + tok["access_token"]}), timeout=60) as r:
384
+ ui = json.loads(r.read())
385
+ resp = RedirectResponse(nxt)
386
+ set_session(resp, {"user": ui.get("preferred_username") or ui.get("sub"),
387
+ "name": ui.get("name", ""), "pic": ui.get("picture", "")})
388
+ return resp
389
+
390
+
391
+ @app.get("/logout")
392
+ def logout():
393
+ r = RedirectResponse("/")
394
+ r.delete_cookie(COOKIE, **COOKIE_KW)
395
+ return r
396
+
397
+
398
+ @app.get("/api/me")
399
+ def api_me(request: Request):
400
+ u = who(request)
401
+ return {"user": u, "login_enabled": bool(OAUTH_ID), "daily_cap": DAILY_CAP}
402
+
403
+
404
+ # ------------------------------------------------------------------ ์ œ์ถœ
405
+ class Submission(BaseModel):
406
+ board: str
407
+ method: str # ๋ฐฉ๋ฒ• ์ด๋ฆ„. ์ˆœ์œ„ํ‘œ์— ์ด๋ ‡๊ฒŒ ํ‘œ์‹œ๋œ๋‹ค
408
+ predictions: dict # {compound_id: ์˜ˆ์ธก๊ฐ’}
409
+ training_data: str = "" # 5์กฐ ๋ˆ„์ถœ๊ฒ€์‚ฌ์— ์“ด๋‹ค
410
+ pretrained_on: str = ""
411
+ code_url: str = ""
412
+ container: str = "" # ์žˆ์œผ๋ฉด [๊ฒ€์ฆ๋จ] ์‹ฌ์‚ฌ ๋Œ€์ƒ (6์กฐ)
413
+
414
+
415
+ @app.post("/api/submit")
416
+ def submit(s: Submission, request: Request):
417
+ u = who(request)
418
+ if not u:
419
+ raise HTTPException(401, "์ œ์ถœํ•˜๋ ค๋ฉด ๋กœ๊ทธ์ธํ•ด์•ผ ํ•œ๋‹ค")
420
+ if not HF_TOKEN:
421
+ raise HTTPException(503, "์›์žฅ์ด ๊ตฌ์„ฑ๋˜์ง€ ์•Š์•˜๋‹ค")
422
+ b = boards().get(s.board)
423
+ if not b:
424
+ raise HTTPException(404, "๊ทธ๋Ÿฐ ๋ถ€๋ฌธ์ด ์—†๋‹ค")
425
+ if not re.fullmatch(r"[\w .\-+/()]{2,60}", s.method or ""):
426
+ raise HTTPException(400, "๋ฐฉ๋ฒ• ์ด๋ฆ„์€ 2~60์ž์—ฌ์•ผ ํ•œ๋‹ค")
427
+ # ํ•˜๋ฃจ ํ•œ๋„. 8์กฐ๊ฐ€ ์ ์ˆ˜ ๊ณต๊ฐœ๋ฅผ ๋ง‰์•„๋„ ์ œ์ถœ ์ž์ฒด๋Š” ๋น„์šฉ์ด ๋“ค๊ณ , ๋ฌด์ œํ•œ์ด๋ฉด
428
+ # ์›์žฅ์ด ์ž ๊ธด๋‹ค. ๊ฒฝ๊ณ„๋Š” ์ฐธ๊ฐ€์ž๊ฐ€ ๊ฐ€์ •ํ•  ํ•˜๋ฃจ(KST)๋กœ ์žก๋Š”๋‹ค.
429
+ day = int((time.time() + 9 * 3600) // 86400)
430
+ key = "cap:%s:%d" % (u["user"], day)
431
+ used = _CAP.get(key, 0)
432
+ if used >= DAILY_CAP:
433
+ raise HTTPException(429, "์˜ค๋Š˜ ์ œ์ถœ ํ•œ๋„ %dํšŒ๋ฅผ ๋‹ค ์ผ๋‹ค. ํ•œ๊ตญ์‹œ๊ฐ„ ์ž์ •์— ์ดˆ๊ธฐํ™”๋œ๋‹ค"
434
+ % DAILY_CAP)
435
+
436
+ n = b.get("n_test", 0)
437
+ got = len(s.predictions or {})
438
+ if got < n:
439
+ raise HTTPException(400, "์˜ˆ์ธก์ด %d๊ฐœ ํ•„์š”ํ•œ๋ฐ %d๊ฐœ๋‹ค. ๋นˆ ํ•ญ๋ชฉ์€ ์ฑ„์›Œ ๋ณด๋‚ด๋ผ "
440
+ "- ์ž„์˜๋กœ ๋ฉ”์šฐ๋ฉด ๊ทธ ๊ฐ’์ด ์ ์ˆ˜์— ๋“ค์–ด๊ฐ„๋‹ค" % (n, got))
441
+ sid = hashlib.sha256(("%s|%s|%s|%f" % (s.board, u["user"], s.method, time.time()))
442
+ .encode()).hexdigest()[:16].upper()
443
+ rec = {"submission_id": sid, "board": s.board, "hf_user": u["user"],
444
+ "method": s.method, "predictions": s.predictions,
445
+ "training_data": s.training_data, "pretrained_on": s.pretrained_on,
446
+ "code_url": s.code_url, "container": s.container,
447
+ "submitted_at": int(time.time()), "spec": SPEC,
448
+ "dataset_sha": b.get("dataset_sha")}
449
+ ledger_write("submissions/%s/%s.json" % (s.board.lower(), sid), rec,
450
+ "submit %s %s" % (s.board, sid))
451
+ _CAP[key] = used + 1
452
+ return {"ok": True, "submission_id": sid,
453
+ "remaining_today": DAILY_CAP - _CAP[key],
454
+ "note": "์ฑ„์ ์€ ์›Œ์ปค๊ฐ€ ํ•œ๋‹ค. 8์กฐ์— ๋”ฐ๋ผ ์ง์ „ ์ตœ๊ณ ์ ์„ ์žก์Œ ๋ฐ”๋‹ฅ ์ด์ƒ์œผ๋กœ "
455
+ "๋„˜์—ˆ์„ ๋•Œ๋งŒ ์ƒˆ ์ ์ˆ˜๊ฐ€ ๊ณต๊ฐœ๋œ๋‹ค."}
456
+
457
+
458
+ @app.get("/api/health")
459
+ def health():
460
+ return {"ok": True, "spec": SPEC, "boards": len(boards()),
461
+ "ledger": bool(HF_TOKEN), "login": bool(OAUTH_ID),
462
+ "scoring_here": False}
i18n.js CHANGED
@@ -59,7 +59,7 @@ ko: {
59
  all: "์ „์ฒด",
60
  tabRank: "์ˆœ์œ„", tabSubmit: "์ œ์ถœ", tabInfo: "๋ถ€๋ฌธ ์ •๋ณด",
61
  thRank: "#", thMethod: "๋ฐฉ๋ฒ•", thUser: "์ œ์ถœ์ž", thLeak: "๋ˆ„์ถœ", thGrade: "๋“ฑ๊ธ‰",
62
- baseline: "๊ธฐ์ค€์„ ", submitted: "์ œ์ถœ๋จ", verified: "๊ฒ€์ฆ๋จ",
63
  leakHigh: "์กฐํšŒ ํ”์ ", leakElev: "๊ทผ์ ‘ ๊ณผ๋ฐ€",
64
  noiseNote: v => `<b>์‹คํ—˜ ์žฌํ˜„ ์˜ค์ฐจ ${v}.</b> ์ด ํญ ์•ˆ์˜ ์ฐจ์ด๋Š” ์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ` +
65
  "์•Š์Šต๋‹ˆ๋‹ค (4์กฐ). ๊ธฐ์ค€์„  3์ข…์€ ์ˆœ์œ„ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค (3์กฐ).",
@@ -231,7 +231,7 @@ en: {
231
  all: "All",
232
  tabRank: "Leaderboard", tabSubmit: "Submit", tabInfo: "Board detail",
233
  thRank: "#", thMethod: "Method", thUser: "Entrant", thLeak: "Leakage", thGrade: "Grade",
234
- baseline: "baseline", submitted: "submitted", verified: "verified",
235
  leakHigh: "lookup trace", leakElev: "near-exact density",
236
  noiseNote: v => `<b>Experimental error ${v}.</b> Differences within this band are not ` +
237
  "claimed as rank differences (clause 4). The three baselines stay resident (clause 3).",
 
59
  all: "์ „์ฒด",
60
  tabRank: "์ˆœ์œ„", tabSubmit: "์ œ์ถœ", tabInfo: "๋ถ€๋ฌธ ์ •๋ณด",
61
  thRank: "#", thMethod: "๋ฐฉ๋ฒ•", thUser: "์ œ์ถœ์ž", thLeak: "๋ˆ„์ถœ", thGrade: "๋“ฑ๊ธ‰",
62
+ baseline: "๊ธฐ์ค€์„ ", submitted: "์ œ์ถœ๋จ", verified: "๊ฒ€์ฆ๋จ", reference: "์ฐธ์กฐ ๋„๊ตฌ",
63
  leakHigh: "์กฐํšŒ ํ”์ ", leakElev: "๊ทผ์ ‘ ๊ณผ๋ฐ€",
64
  noiseNote: v => `<b>์‹คํ—˜ ์žฌํ˜„ ์˜ค์ฐจ ${v}.</b> ์ด ํญ ์•ˆ์˜ ์ฐจ์ด๋Š” ์ˆœ์œ„ ์ฐจ์ด๋กœ ์ฃผ์žฅํ•˜์ง€ ` +
65
  "์•Š์Šต๋‹ˆ๋‹ค (4์กฐ). ๊ธฐ์ค€์„  3์ข…์€ ์ˆœ์œ„ํ‘œ์—์„œ ๋‚ด๋ ค๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค (3์กฐ).",
 
231
  all: "All",
232
  tabRank: "Leaderboard", tabSubmit: "Submit", tabInfo: "Board detail",
233
  thRank: "#", thMethod: "Method", thUser: "Entrant", thLeak: "Leakage", thGrade: "Grade",
234
+ baseline: "baseline", submitted: "submitted", verified: "verified", reference: "reference tool",
235
  leakHigh: "lookup trace", leakElev: "near-exact density",
236
  noiseNote: v => `<b>Experimental error ${v}.</b> Differences within this band are not ` +
237
  "claimed as rank differences (clause 4). The three baselines stay resident (clause 3).",
index.html CHANGED
@@ -492,13 +492,14 @@ async function paneRank(b){
492
  const rows=d.rows.map(r=>{
493
  const g=r.rank&&r.rank<=3?` g${r.rank}`:"";
494
  const w=Math.max(4,58*(r.mae||0)/worst);
495
- return `<tr class="${r.kind==="baseline"?"base":""}">
496
  <td>${r.rank?`<span class="rk${g}">${r.rank}</span>`:"ยท"}</td>
497
  <td>${esc(r.method)}</td><td>${esc(r.user)}</td>
498
  <td><span class="mini" style="width:${w}px"></span>${f(r.mae)}</td>
499
  <td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
500
  <td>${r.kind==="baseline"?`<span class="chip g">${t.baseline}</span>`:leak(r.leak)}</td>
501
- <td>${r.kind==="baseline"?"โ€”":(r.verified?`<span class="chip ok">${t.verified}</span>`
 
502
  :`<span class="chip g">${t.submitted}</span>`)}</td></tr>`;}).join("");
503
  $("#pane").innerHTML=`
504
  <div class="tw"><table><thead><tr><th>${t.thRank}</th><th>${t.thMethod}</th><th>${t.thUser}</th>
 
492
  const rows=d.rows.map(r=>{
493
  const g=r.rank&&r.rank<=3?` g${r.rank}`:"";
494
  const w=Math.max(4,58*(r.mae||0)/worst);
495
+ return `<tr class="${r.kind!=="entry"?"base":""}">
496
  <td>${r.rank?`<span class="rk${g}">${r.rank}</span>`:"ยท"}</td>
497
  <td>${esc(r.method)}</td><td>${esc(r.user)}</td>
498
  <td><span class="mini" style="width:${w}px"></span>${f(r.mae)}</td>
499
  <td>${f(r.auroc)}</td><td>${f(r.prauc)}</td>
500
  <td>${r.kind==="baseline"?`<span class="chip g">${t.baseline}</span>`:leak(r.leak)}</td>
501
+ <td>${r.kind==="baseline"?"โ€”":r.kind==="reference"?`<span class="chip wr">${t.reference}</span>`
502
+ :(r.verified?`<span class="chip ok">${t.verified}</span>`
503
  :`<span class="chip g">${t.submitted}</span>`)}</td></tr>`;}).join("");
504
  $("#pane").innerHTML=`
505
  <div class="tw"><table><thead><tr><th>${t.thRank}</th><th>${t.thMethod}</th><th>${t.thUser}</th>