XLearning-SCU abidlabs HF Staff commited on
Commit
a425bcf
·
0 Parent(s):

Duplicate from gradio-templates/leaderboard

Browse files

Co-authored-by: Abubakar Abid <abidlabs@users.noreply.huggingface.co>

.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tflite filter=lfs diff=lfs merge=lfs -text
29
+ *.tgz filter=lfs diff=lfs merge=lfs -text
30
+ *.wasm filter=lfs diff=lfs merge=lfs -text
31
+ *.xz filter=lfs diff=lfs merge=lfs -text
32
+ *.zip filter=lfs diff=lfs merge=lfs -text
33
+ *.zst filter=lfs diff=lfs merge=lfs -text
34
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
35
+ scale-hf-logo.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ auto_evals/
2
+ venv/
3
+ __pycache__/
4
+ .env
5
+ .ipynb_checkpoints
6
+ *ipynb
7
+ .vscode/
8
+
9
+ eval-queue/
10
+ eval-results/
11
+ eval-queue-bk/
12
+ eval-results-bk/
13
+ logs/
.pre-commit-config.yaml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2022, NVIDIA CORPORATION. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ default_language_version:
16
+ python: python3
17
+
18
+ ci:
19
+ autofix_prs: true
20
+ autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions'
21
+ autoupdate_schedule: quarterly
22
+
23
+ repos:
24
+ - repo: https://github.com/pre-commit/pre-commit-hooks
25
+ rev: v4.3.0
26
+ hooks:
27
+ - id: check-yaml
28
+ - id: check-case-conflict
29
+ - id: detect-private-key
30
+ - id: check-added-large-files
31
+ args: ['--maxkb=1000']
32
+ - id: requirements-txt-fixer
33
+ - id: end-of-file-fixer
34
+ - id: trailing-whitespace
35
+
36
+ - repo: https://github.com/PyCQA/isort
37
+ rev: 5.12.0
38
+ hooks:
39
+ - id: isort
40
+ name: Format imports
41
+
42
+ - repo: https://github.com/psf/black
43
+ rev: 22.12.0
44
+ hooks:
45
+ - id: black
46
+ name: Format code
47
+ additional_dependencies: ['click==8.0.2']
48
+
49
+ - repo: https://github.com/charliermarsh/ruff-pre-commit
50
+ # Ruff version.
51
+ rev: 'v0.0.267'
52
+ hooks:
53
+ - id: ruff
Makefile ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .PHONY: style format
2
+
3
+
4
+ style:
5
+ python -m black --line-length 119 .
6
+ python -m isort .
7
+ ruff check --fix .
8
+
9
+
10
+ quality:
11
+ python -m black --check --line-length 119 .
12
+ python -m isort --check-only .
13
+ ruff check .
README.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Example Leaderboard Template
3
+ emoji: 🥇
4
+ colorFrom: green
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ app_file: app.py
8
+ pinned: true
9
+ license: apache-2.0
10
+ short_description: Duplicate this leaderboard to initialize your own!
11
+ sdk_version: 6.26.0
12
+ tags:
13
+ - leaderboard
14
+ ---
15
+
16
+ # Start the configuration
17
+
18
+ This template uses Gradio 6 Server mode: a custom responsive frontend backed by named Gradio API endpoints. Duplicate the Space, then customize the copy and benchmark definitions below.
19
+
20
+ Most of the variables to change for a default leaderboard are in `src/envs.py` (replace the paths for your leaderboard) and `src/about.py` (for its title, copy, and tasks).
21
+
22
+ Results files should have the following format and be stored as json files:
23
+ ```json
24
+ {
25
+ "config": {
26
+ "model_dtype": "torch.float16", # or torch.bfloat16 or 8bit or 4bit
27
+ "model_name": "path of the model on the hub: org/model",
28
+ "model_sha": "revision on the hub",
29
+ },
30
+ "results": {
31
+ "task_name": {
32
+ "metric_name": score,
33
+ },
34
+ "task_name2": {
35
+ "metric_name": score,
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ Request files are created automatically by this tool.
42
+
43
+ If you encounter problem on the space, don't hesitate to restart it to remove the create eval-queue, eval-queue-bk, eval-results and eval-results-bk created folder.
44
+
45
+ # Code logic for more complex edits
46
+
47
+ You'll find
48
+ - the main table' columns names and properties in `src/display/utils.py`
49
+ - the logic to read all results and request files, then convert them in dataframe lines, in `src/leaderboard/read_evals.py`, and `src/populate.py`
50
+ - the logic to allow or filter submissions in `src/submission/submit.py` and `src/submission/check_validity.py`
51
+ - the custom frontend in `static/index.html`, `static/styles.css`, and `static/app.js`
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import html
4
+ import logging
5
+ import re
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from threading import RLock
9
+ from typing import Any
10
+
11
+ import gradio as gr
12
+ import pandas as pd
13
+ from apscheduler.schedulers.background import BackgroundScheduler
14
+ from fastapi.responses import HTMLResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+ from huggingface_hub import snapshot_download
17
+
18
+ from src.about import (
19
+ CITATION_BUTTON_LABEL,
20
+ CITATION_BUTTON_TEXT,
21
+ EVALUATION_QUEUE_TEXT,
22
+ LLM_BENCHMARKS_TEXT,
23
+ TITLE,
24
+ Tasks,
25
+ )
26
+ from src.display.utils import (
27
+ BENCHMARK_COLS,
28
+ COLS,
29
+ EVAL_COLS,
30
+ ModelType,
31
+ Precision,
32
+ WeightType,
33
+ )
34
+ from src.envs import EVAL_REQUESTS_PATH, EVAL_RESULTS_PATH, QUEUE_REPO, RESULTS_REPO, TOKEN
35
+ from src.populate import get_evaluation_queue_df, get_leaderboard_df
36
+ from src.submission.submit import add_new_eval
37
+
38
+ logging.basicConfig(level=logging.INFO)
39
+ LOGGER = logging.getLogger("leaderboard")
40
+
41
+ ROOT = Path(__file__).parent
42
+ STATIC_DIR = ROOT / "static"
43
+ MODEL_LINK_RE = re.compile(r'href=["\']https://huggingface\.co/([^"\']+)["\']')
44
+ DATA_LOCK = RLock()
45
+ DATA: dict[str, Any] = {}
46
+
47
+
48
+ def _download_dataset(repo_id: str, local_dir: str) -> None:
49
+ Path(local_dir).mkdir(parents=True, exist_ok=True)
50
+ snapshot_download(
51
+ repo_id=repo_id,
52
+ local_dir=local_dir,
53
+ repo_type="dataset",
54
+ tqdm_class=None,
55
+ etag_timeout=30,
56
+ token=TOKEN,
57
+ )
58
+
59
+
60
+ def _json_value(value: Any) -> Any:
61
+ if pd.isna(value):
62
+ return None
63
+ if hasattr(value, "item"):
64
+ value = value.item()
65
+ return value
66
+
67
+
68
+ def _model_id(value: Any) -> str:
69
+ text = str(value or "")
70
+ if match := MODEL_LINK_RE.search(text):
71
+ return html.unescape(match.group(1))
72
+ return html.unescape(re.sub("<[^>]+>", "", text))
73
+
74
+
75
+ def _records(frame: pd.DataFrame) -> list[dict[str, Any]]:
76
+ records = []
77
+ for row in frame.to_dict(orient="records"):
78
+ clean_row = {str(key): _json_value(value) for key, value in row.items()}
79
+ if "Model" in clean_row:
80
+ clean_row["Model"] = _model_id(clean_row["Model"])
81
+ if "model" in clean_row:
82
+ clean_row["model"] = _model_id(clean_row["model"])
83
+ records.append(clean_row)
84
+ return records
85
+
86
+
87
+ def _empty_payload(message: str | None = None) -> dict[str, Any]:
88
+ return {
89
+ "rows": [],
90
+ "queues": {"finished": [], "running": [], "pending": []},
91
+ "benchmarks": [task.value.col_name for task in Tasks],
92
+ "model_types": [item.to_str(" · ") for item in ModelType if item is not ModelType.Unknown],
93
+ "precisions": [item.value.name for item in Precision if item is not Precision.Unknown],
94
+ "weight_types": [item.value.name for item in WeightType],
95
+ "copy": {
96
+ "title": TITLE,
97
+ "about": LLM_BENCHMARKS_TEXT.strip(),
98
+ "submission": EVALUATION_QUEUE_TEXT.strip(),
99
+ "citation_label": CITATION_BUTTON_LABEL,
100
+ "citation": CITATION_BUTTON_TEXT.strip(),
101
+ },
102
+ "updated_at": datetime.now(timezone.utc).isoformat(),
103
+ "error": message,
104
+ }
105
+
106
+
107
+ def refresh_data(*, download: bool = True) -> dict[str, Any]:
108
+ """Refresh leaderboard and queue data without taking the Space offline."""
109
+ errors = []
110
+ if download:
111
+ for repo_id, local_dir in (
112
+ (QUEUE_REPO, EVAL_REQUESTS_PATH),
113
+ (RESULTS_REPO, EVAL_RESULTS_PATH),
114
+ ):
115
+ try:
116
+ _download_dataset(repo_id, local_dir)
117
+ except Exception as exc: # Keep serving the last successful snapshot.
118
+ LOGGER.exception("Could not refresh %s", repo_id)
119
+ errors.append(f"Could not refresh {repo_id}: {exc}")
120
+
121
+ payload = _empty_payload("; ".join(errors) or None)
122
+ try:
123
+ leaderboard = get_leaderboard_df(EVAL_RESULTS_PATH, EVAL_REQUESTS_PATH, COLS, BENCHMARK_COLS)
124
+ payload["rows"] = _records(leaderboard)
125
+ except Exception as exc:
126
+ LOGGER.exception("Could not build leaderboard data")
127
+ payload["error"] = f"Could not build leaderboard data: {exc}"
128
+
129
+ try:
130
+ finished, running, pending = get_evaluation_queue_df(EVAL_REQUESTS_PATH, EVAL_COLS)
131
+ payload["queues"] = {
132
+ "finished": _records(finished),
133
+ "running": _records(running),
134
+ "pending": _records(pending),
135
+ }
136
+ except Exception as exc:
137
+ LOGGER.exception("Could not build evaluation queues")
138
+ payload["error"] = f"Could not build evaluation queues: {exc}"
139
+
140
+ with DATA_LOCK:
141
+ DATA.clear()
142
+ DATA.update(payload)
143
+ return dict(DATA)
144
+
145
+
146
+ server = gr.Server(
147
+ title="Open LLM Benchmark",
148
+ description="A clean, customizable Gradio 6 leaderboard template.",
149
+ version="2.0.0",
150
+ )
151
+ server.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
152
+
153
+
154
+ @server.get("/", response_class=HTMLResponse, include_in_schema=False)
155
+ async def homepage() -> HTMLResponse:
156
+ return HTMLResponse((STATIC_DIR / "index.html").read_text())
157
+
158
+
159
+ @server.api(name="leaderboard_data", queue=False, concurrency_limit=None)
160
+ def leaderboard_data() -> dict[str, Any]:
161
+ """Return the current leaderboard, evaluation queues, filters, and template copy."""
162
+ with DATA_LOCK:
163
+ return dict(DATA) if DATA else _empty_payload("Data is still loading.")
164
+
165
+
166
+ @server.api(name="submit_model", concurrency_limit=4)
167
+ def submit_model(
168
+ model: str,
169
+ base_model: str,
170
+ revision: str,
171
+ precision: str,
172
+ weight_type: str,
173
+ model_type: str,
174
+ ) -> dict[str, str]:
175
+ """Validate a public Hugging Face model and add it to the evaluation queue."""
176
+ result = add_new_eval(model, base_model, revision, precision, weight_type, model_type)
177
+ if result["status"] == "success":
178
+ refresh_data(download=False)
179
+ return result
180
+
181
+
182
+ if __name__ == "__main__":
183
+ refresh_data()
184
+ scheduler = BackgroundScheduler(daemon=True)
185
+ scheduler.add_job(refresh_data, "interval", minutes=10, max_instances=1, coalesce=True)
186
+ scheduler.start()
187
+ server.launch()
pyproject.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.ruff]
2
+ line-length = 119
3
+
4
+ [tool.ruff.lint]
5
+ select = ["E", "F"]
6
+ ignore = ["E501"] # line too long (black is taking care of this)
7
+ fixable = ["A", "B", "C", "D", "E", "F", "G", "I", "N", "Q", "S", "T", "W", "ANN", "ARG", "BLE", "COM", "DJ", "DTZ", "EM", "ERA", "EXE", "FBT", "ICN", "INP", "ISC", "NPY", "PD", "PGH", "PIE", "PL", "PT", "PTH", "PYI", "RET", "RSE", "RUF", "SIM", "SLF", "TCH", "TID", "TRY", "UP", "YTT"]
8
+
9
+ [tool.isort]
10
+ profile = "black"
11
+ line_length = 119
12
+
13
+ [tool.black]
14
+ line-length = 119
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ APScheduler
2
+ gradio==6.26.0
3
+ huggingface-hub>=0.18.0
4
+ numpy
5
+ pandas
6
+ python-dateutil
7
+ sentencepiece
8
+ tokenizers>=0.15.0
9
+ transformers
src/about.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+
5
+ @dataclass
6
+ class Task:
7
+ benchmark: str
8
+ metric: str
9
+ col_name: str
10
+
11
+
12
+ # Select your tasks here
13
+ # ---------------------------------------------------
14
+ class Tasks(Enum):
15
+ # task_key in the json file, metric_key in the json file, name to display in the leaderboard
16
+ task0 = Task("anli_r1", "acc", "ANLI")
17
+ task1 = Task("logiqa", "acc_norm", "LogiQA")
18
+
19
+
20
+ NUM_FEWSHOT = 0 # Change with your few shot
21
+ # ---------------------------------------------------
22
+
23
+
24
+ # Your leaderboard name
25
+ TITLE = "Open Leaderboard"
26
+
27
+ # Which evaluations are you running? how can people reproduce what you have?
28
+ LLM_BENCHMARKS_TEXT = """
29
+ ## How it works
30
+
31
+ ## Reproducibility
32
+ To reproduce our results, here is the commands you can run:
33
+
34
+ """
35
+
36
+ EVALUATION_QUEUE_TEXT = """
37
+ ## Some good practices before submitting a model
38
+
39
+ ### 1) Make sure you can load your model and tokenizer using AutoClasses:
40
+ ```python
41
+ from transformers import AutoConfig, AutoModel, AutoTokenizer
42
+ config = AutoConfig.from_pretrained("your model name", revision=revision)
43
+ model = AutoModel.from_pretrained("your model name", revision=revision)
44
+ tokenizer = AutoTokenizer.from_pretrained("your model name", revision=revision)
45
+ ```
46
+ If this step fails, follow the error messages to debug your model before submitting it. It's likely your model has been improperly uploaded.
47
+
48
+ Note: make sure your model is public!
49
+ Note: if your model needs `use_remote_code=True`, we do not support this option yet but we are working on adding it, stay posted!
50
+
51
+ ### 2) Convert your model weights to [safetensors](https://huggingface.co/docs/safetensors/index)
52
+ It's a new format for storing weights which is safer and faster to load and use. It will also allow us to add the number of parameters of your model to the `Extended Viewer`!
53
+
54
+ ### 3) Make sure your model has an open license!
55
+ This is a leaderboard for Open LLMs, and we'd love for as many people as possible to know they can use your model 🤗
56
+
57
+ ### 4) Fill up your model card
58
+ When we add extra information about models to the leaderboard, it will be automatically taken from the model card
59
+
60
+ ## In case of model failure
61
+ If your model is displayed in the `FAILED` category, its execution stopped.
62
+ Make sure you have followed the above steps first.
63
+ If everything is done, check you can launch the EleutherAIHarness on your model locally, using the above command without modifications (you can add `--limit` to limit the number of examples per task).
64
+ """
65
+
66
+ CITATION_BUTTON_LABEL = "Copy the following snippet to cite these results"
67
+ CITATION_BUTTON_TEXT = r"""
68
+ """
src/display/formatting.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def model_hyperlink(link, model_name):
2
+ return f'<a target="_blank" href="{link}" style="color: var(--link-text-color); text-decoration: underline;text-decoration-style: dotted;">{model_name}</a>'
3
+
4
+
5
+ def make_clickable_model(model_name):
6
+ link = f"https://huggingface.co/{model_name}"
7
+ return model_hyperlink(link, model_name)
8
+
9
+
10
+ def has_no_nan_values(df, columns):
11
+ return df[columns].notna().all(axis=1)
12
+
13
+
14
+ def has_nan_values(df, columns):
15
+ return df[columns].isna().any(axis=1)
src/display/utils.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, make_dataclass
2
+ from enum import Enum
3
+
4
+ from src.about import Tasks
5
+
6
+
7
+ def fields(raw_class):
8
+ return [v for k, v in raw_class.__dict__.items() if k[:2] != "__" and k[-2:] != "__"]
9
+
10
+
11
+ # These classes are for user facing column names,
12
+ # to avoid having to change them all around the code
13
+ # when a modif is needed
14
+ @dataclass(frozen=True)
15
+ class ColumnContent:
16
+ name: str
17
+ type: str
18
+ displayed_by_default: bool
19
+ hidden: bool = False
20
+ never_hidden: bool = False
21
+
22
+
23
+ ## Leaderboard columns
24
+ auto_eval_column_dict = []
25
+ # Init
26
+ auto_eval_column_dict.append(["model_type_symbol", ColumnContent, ColumnContent("T", "str", True, never_hidden=True)])
27
+ auto_eval_column_dict.append(["model", ColumnContent, ColumnContent("Model", "markdown", True, never_hidden=True)])
28
+ # Scores
29
+ auto_eval_column_dict.append(["average", ColumnContent, ColumnContent("Average ⬆️", "number", True)])
30
+ for task in Tasks:
31
+ auto_eval_column_dict.append([task.name, ColumnContent, ColumnContent(task.value.col_name, "number", True)])
32
+ # Model information
33
+ auto_eval_column_dict.append(["model_type", ColumnContent, ColumnContent("Type", "str", False)])
34
+ auto_eval_column_dict.append(["architecture", ColumnContent, ColumnContent("Architecture", "str", False)])
35
+ auto_eval_column_dict.append(["weight_type", ColumnContent, ColumnContent("Weight type", "str", False, True)])
36
+ auto_eval_column_dict.append(["precision", ColumnContent, ColumnContent("Precision", "str", False)])
37
+ auto_eval_column_dict.append(["license", ColumnContent, ColumnContent("Hub License", "str", False)])
38
+ auto_eval_column_dict.append(["params", ColumnContent, ColumnContent("#Params (B)", "number", False)])
39
+ auto_eval_column_dict.append(["likes", ColumnContent, ColumnContent("Hub ❤️", "number", False)])
40
+ auto_eval_column_dict.append(["still_on_hub", ColumnContent, ColumnContent("Available on the hub", "bool", False)])
41
+ auto_eval_column_dict.append(["revision", ColumnContent, ColumnContent("Model sha", "str", False, False)])
42
+
43
+ # We use make dataclass to dynamically fill the scores from Tasks
44
+ AutoEvalColumn = make_dataclass("AutoEvalColumn", auto_eval_column_dict, frozen=True)
45
+
46
+
47
+ ## For the queue columns in the submission tab
48
+ @dataclass(frozen=True)
49
+ class EvalQueueColumn: # Queue column
50
+ model = ColumnContent("model", "markdown", True)
51
+ revision = ColumnContent("revision", "str", True)
52
+ private = ColumnContent("private", "bool", True)
53
+ precision = ColumnContent("precision", "str", True)
54
+ weight_type = ColumnContent("weight_type", "str", "Original")
55
+ status = ColumnContent("status", "str", True)
56
+
57
+
58
+ ## All the model information that we might need
59
+ @dataclass
60
+ class ModelDetails:
61
+ name: str
62
+ display_name: str = ""
63
+ symbol: str = "" # emoji
64
+
65
+
66
+ class ModelType(Enum):
67
+ PT = ModelDetails(name="pretrained", symbol="🟢")
68
+ FT = ModelDetails(name="fine-tuned", symbol="🔶")
69
+ IFT = ModelDetails(name="instruction-tuned", symbol="⭕")
70
+ RL = ModelDetails(name="RL-tuned", symbol="🟦")
71
+ Unknown = ModelDetails(name="", symbol="?")
72
+
73
+ def to_str(self, separator=" "):
74
+ return f"{self.value.symbol}{separator}{self.value.name}"
75
+
76
+ @staticmethod
77
+ def from_str(type):
78
+ if "fine-tuned" in type or "🔶" in type:
79
+ return ModelType.FT
80
+ if "pretrained" in type or "🟢" in type:
81
+ return ModelType.PT
82
+ if "RL-tuned" in type or "🟦" in type:
83
+ return ModelType.RL
84
+ if "instruction-tuned" in type or "⭕" in type:
85
+ return ModelType.IFT
86
+ return ModelType.Unknown
87
+
88
+
89
+ class WeightType(Enum):
90
+ Adapter = ModelDetails("Adapter")
91
+ Original = ModelDetails("Original")
92
+ Delta = ModelDetails("Delta")
93
+
94
+
95
+ class Precision(Enum):
96
+ float16 = ModelDetails("float16")
97
+ bfloat16 = ModelDetails("bfloat16")
98
+ Unknown = ModelDetails("?")
99
+
100
+ def from_str(precision):
101
+ if precision in ["torch.float16", "float16"]:
102
+ return Precision.float16
103
+ if precision in ["torch.bfloat16", "bfloat16"]:
104
+ return Precision.bfloat16
105
+ return Precision.Unknown
106
+
107
+
108
+ # Column selection
109
+ COLS = [c.name for c in fields(AutoEvalColumn) if not c.hidden]
110
+
111
+ EVAL_COLS = [c.name for c in fields(EvalQueueColumn)]
112
+ EVAL_TYPES = [c.type for c in fields(EvalQueueColumn)]
113
+
114
+ BENCHMARK_COLS = [t.value.col_name for t in Tasks]
src/envs.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from huggingface_hub import HfApi
4
+
5
+ # Info to change for your repository
6
+ # ----------------------------------
7
+ TOKEN = os.environ.get("HF_TOKEN") # A read/write token for your org
8
+
9
+ OWNER = "demo-leaderboard-backend" # Change to your org - don't forget to create a results and request dataset, with the correct format!
10
+ # ----------------------------------
11
+
12
+ QUEUE_REPO = f"{OWNER}/requests"
13
+ RESULTS_REPO = f"{OWNER}/results"
14
+
15
+ # If you setup a cache later, just change HF_HOME
16
+ CACHE_PATH = os.getenv("HF_HOME", ".")
17
+
18
+ # Local caches
19
+ EVAL_REQUESTS_PATH = os.path.join(CACHE_PATH, "eval-queue")
20
+ EVAL_RESULTS_PATH = os.path.join(CACHE_PATH, "eval-results")
21
+ EVAL_REQUESTS_PATH_BACKEND = os.path.join(CACHE_PATH, "eval-queue-bk")
22
+ EVAL_RESULTS_PATH_BACKEND = os.path.join(CACHE_PATH, "eval-results-bk")
23
+
24
+ API = HfApi(token=TOKEN)
src/leaderboard/read_evals.py ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import glob
2
+ import json
3
+ import os
4
+ from dataclasses import dataclass
5
+
6
+ import dateutil
7
+ import numpy as np
8
+
9
+ from src.display.formatting import make_clickable_model
10
+ from src.display.utils import AutoEvalColumn, ModelType, Tasks, Precision, WeightType
11
+ from src.submission.check_validity import is_model_on_hub
12
+
13
+
14
+ @dataclass
15
+ class EvalResult:
16
+ """Represents one full evaluation. Built from a combination of the result and request file for a given run."""
17
+
18
+ eval_name: str # org_model_precision (uid)
19
+ full_model: str # org/model (path on hub)
20
+ org: str
21
+ model: str
22
+ revision: str # commit hash, "" if main
23
+ results: dict
24
+ precision: Precision = Precision.Unknown
25
+ model_type: ModelType = ModelType.Unknown # Pretrained, fine tuned, ...
26
+ weight_type: WeightType = WeightType.Original # Original or Adapter
27
+ architecture: str = "Unknown"
28
+ license: str = "?"
29
+ likes: int = 0
30
+ num_params: int = 0
31
+ date: str = "" # submission date of request file
32
+ still_on_hub: bool = False
33
+
34
+ @classmethod
35
+ def init_from_json_file(self, json_filepath):
36
+ """Inits the result from the specific model result file"""
37
+ with open(json_filepath) as fp:
38
+ data = json.load(fp)
39
+
40
+ config = data.get("config")
41
+
42
+ # Precision
43
+ precision = Precision.from_str(config.get("model_dtype"))
44
+
45
+ # Get model and org
46
+ org_and_model = config.get("model_name", config.get("model_args", None))
47
+ org_and_model = org_and_model.split("/", 1)
48
+
49
+ if len(org_and_model) == 1:
50
+ org = None
51
+ model = org_and_model[0]
52
+ result_key = f"{model}_{precision.value.name}"
53
+ else:
54
+ org = org_and_model[0]
55
+ model = org_and_model[1]
56
+ result_key = f"{org}_{model}_{precision.value.name}"
57
+ full_model = "/".join(org_and_model)
58
+
59
+ still_on_hub, _, model_config = is_model_on_hub(
60
+ full_model, config.get("model_sha", "main"), trust_remote_code=True, test_tokenizer=False
61
+ )
62
+ architecture = "?"
63
+ if model_config is not None:
64
+ architectures = getattr(model_config, "architectures", None)
65
+ if architectures:
66
+ architecture = ";".join(architectures)
67
+
68
+ # Extract results available in this file (some results are split in several files)
69
+ results = {}
70
+ for task in Tasks:
71
+ task = task.value
72
+
73
+ # We average all scores of a given metric (not all metrics are present in all files)
74
+ accs = np.array([v.get(task.metric, None) for k, v in data["results"].items() if task.benchmark == k])
75
+ if accs.size == 0 or any([acc is None for acc in accs]):
76
+ continue
77
+
78
+ mean_acc = np.mean(accs) * 100.0
79
+ results[task.benchmark] = mean_acc
80
+
81
+ return self(
82
+ eval_name=result_key,
83
+ full_model=full_model,
84
+ org=org,
85
+ model=model,
86
+ results=results,
87
+ precision=precision,
88
+ revision=config.get("model_sha", ""),
89
+ still_on_hub=still_on_hub,
90
+ architecture=architecture,
91
+ )
92
+
93
+ def update_with_request_file(self, requests_path):
94
+ """Finds the relevant request file for the current model and updates info with it"""
95
+ request_file = get_request_file_for_model(requests_path, self.full_model, self.precision.value.name)
96
+
97
+ try:
98
+ with open(request_file, "r") as f:
99
+ request = json.load(f)
100
+ self.model_type = ModelType.from_str(request.get("model_type", ""))
101
+ self.weight_type = WeightType[request.get("weight_type", "Original")]
102
+ self.license = request.get("license", "?")
103
+ self.likes = request.get("likes", 0)
104
+ self.num_params = request.get("params", 0)
105
+ self.date = request.get("submitted_time", "")
106
+ except Exception:
107
+ print(
108
+ f"Could not find request file for {self.org}/{self.model} with precision {self.precision.value.name}"
109
+ )
110
+
111
+ def to_dict(self):
112
+ """Converts the Eval Result to a dict compatible with our dataframe display"""
113
+ average = sum([v for v in self.results.values() if v is not None]) / len(Tasks)
114
+ data_dict = {
115
+ "eval_name": self.eval_name, # not a column, just a save name,
116
+ AutoEvalColumn.precision.name: self.precision.value.name,
117
+ AutoEvalColumn.model_type.name: self.model_type.value.name,
118
+ AutoEvalColumn.model_type_symbol.name: self.model_type.value.symbol,
119
+ AutoEvalColumn.weight_type.name: self.weight_type.value.name,
120
+ AutoEvalColumn.architecture.name: self.architecture,
121
+ AutoEvalColumn.model.name: make_clickable_model(self.full_model),
122
+ AutoEvalColumn.revision.name: self.revision,
123
+ AutoEvalColumn.average.name: average,
124
+ AutoEvalColumn.license.name: self.license,
125
+ AutoEvalColumn.likes.name: self.likes,
126
+ AutoEvalColumn.params.name: self.num_params,
127
+ AutoEvalColumn.still_on_hub.name: self.still_on_hub,
128
+ }
129
+
130
+ for task in Tasks:
131
+ data_dict[task.value.col_name] = self.results[task.value.benchmark]
132
+
133
+ return data_dict
134
+
135
+
136
+ def get_request_file_for_model(requests_path, model_name, precision):
137
+ """Selects the correct request file for a given model. Only keeps runs tagged as FINISHED"""
138
+ request_files = os.path.join(
139
+ requests_path,
140
+ f"{model_name}_eval_request_*.json",
141
+ )
142
+ request_files = glob.glob(request_files)
143
+
144
+ # Select correct request file (precision)
145
+ request_file = ""
146
+ request_files = sorted(request_files, reverse=True)
147
+ for tmp_request_file in request_files:
148
+ with open(tmp_request_file, "r") as f:
149
+ req_content = json.load(f)
150
+ if req_content["status"] in ["FINISHED"] and req_content["precision"] == precision.split(".")[-1]:
151
+ request_file = tmp_request_file
152
+ return request_file
153
+
154
+
155
+ def get_raw_eval_results(results_path: str, requests_path: str) -> list[EvalResult]:
156
+ """From the path of the results folder root, extract all needed info for results"""
157
+ model_result_filepaths = []
158
+
159
+ for root, directories, files in os.walk(results_path):
160
+ directories[:] = [directory for directory in directories if not directory.startswith(".")]
161
+ # We should only have json files in model results
162
+ if len(files) == 0 or any([not f.endswith(".json") for f in files]):
163
+ continue
164
+
165
+ # Sort the files by date
166
+ try:
167
+ files.sort(key=lambda x: x.removesuffix(".json").removeprefix("results_")[:-7])
168
+ except dateutil.parser._parser.ParserError:
169
+ files = [files[-1]]
170
+
171
+ for file in files:
172
+ model_result_filepaths.append(os.path.join(root, file))
173
+
174
+ eval_results = {}
175
+ for model_result_filepath in model_result_filepaths:
176
+ # Creation of result
177
+ eval_result = EvalResult.init_from_json_file(model_result_filepath)
178
+ eval_result.update_with_request_file(requests_path)
179
+
180
+ # Store results of same eval together
181
+ eval_name = eval_result.eval_name
182
+ if eval_name in eval_results.keys():
183
+ eval_results[eval_name].results.update({k: v for k, v in eval_result.results.items() if v is not None})
184
+ else:
185
+ eval_results[eval_name] = eval_result
186
+
187
+ results = []
188
+ for v in eval_results.values():
189
+ try:
190
+ v.to_dict() # we test if the dict version is complete
191
+ results.append(v)
192
+ except KeyError: # not all eval values present
193
+ continue
194
+
195
+ return results
src/populate.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+
4
+ import pandas as pd
5
+
6
+ from src.display.formatting import has_no_nan_values, make_clickable_model
7
+ from src.display.utils import AutoEvalColumn, EvalQueueColumn
8
+ from src.leaderboard.read_evals import get_raw_eval_results
9
+
10
+
11
+ def get_leaderboard_df(results_path: str, requests_path: str, cols: list, benchmark_cols: list) -> pd.DataFrame:
12
+ """Creates a dataframe from all the individual experiment results"""
13
+ raw_data = get_raw_eval_results(results_path, requests_path)
14
+ all_data_json = [v.to_dict() for v in raw_data]
15
+
16
+ df = pd.DataFrame.from_records(all_data_json)
17
+ df = df.sort_values(by=[AutoEvalColumn.average.name], ascending=False)
18
+ df = df[cols].round(decimals=2)
19
+
20
+ # filter out if any of the benchmarks have not been produced
21
+ df = df[has_no_nan_values(df, benchmark_cols)]
22
+ return df
23
+
24
+
25
+ def get_evaluation_queue_df(save_path: str, cols: list) -> list[pd.DataFrame]:
26
+ """Creates the different dataframes for the evaluation queues requestes"""
27
+ entries = [entry for entry in os.listdir(save_path) if not entry.startswith(".")]
28
+ all_evals = []
29
+
30
+ for entry in entries:
31
+ if ".json" in entry:
32
+ file_path = os.path.join(save_path, entry)
33
+ with open(file_path) as fp:
34
+ data = json.load(fp)
35
+
36
+ data[EvalQueueColumn.model.name] = make_clickable_model(data["model"])
37
+ data[EvalQueueColumn.revision.name] = data.get("revision", "main")
38
+
39
+ all_evals.append(data)
40
+ elif ".md" not in entry:
41
+ entry_path = os.path.join(save_path, entry)
42
+ sub_entries = [
43
+ name
44
+ for name in os.listdir(entry_path)
45
+ if os.path.isfile(os.path.join(entry_path, name)) and not name.startswith(".")
46
+ ]
47
+ for sub_entry in sub_entries:
48
+ file_path = os.path.join(entry_path, sub_entry)
49
+ with open(file_path) as fp:
50
+ data = json.load(fp)
51
+
52
+ data[EvalQueueColumn.model.name] = make_clickable_model(data["model"])
53
+ data[EvalQueueColumn.revision.name] = data.get("revision", "main")
54
+ all_evals.append(data)
55
+
56
+ pending_list = [e for e in all_evals if e["status"] in ["PENDING", "RERUN"]]
57
+ running_list = [e for e in all_evals if e["status"] == "RUNNING"]
58
+ finished_list = [e for e in all_evals if e["status"].startswith("FINISHED") or e["status"] == "PENDING_NEW_EVAL"]
59
+ df_pending = pd.DataFrame.from_records(pending_list, columns=cols)
60
+ df_running = pd.DataFrame.from_records(running_list, columns=cols)
61
+ df_finished = pd.DataFrame.from_records(finished_list, columns=cols)
62
+ return df_finished[cols], df_running[cols], df_pending[cols]
src/submission/check_validity.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ from collections import defaultdict
4
+
5
+ import huggingface_hub
6
+ from huggingface_hub import ModelCard
7
+ from huggingface_hub.hf_api import ModelInfo
8
+ from transformers import AutoConfig
9
+ from transformers.models.auto.tokenization_auto import AutoTokenizer
10
+
11
+
12
+ def check_model_card(repo_id: str) -> tuple[bool, str]:
13
+ """Checks if the model card and license exist and have been filled"""
14
+ try:
15
+ card = ModelCard.load(repo_id)
16
+ except huggingface_hub.utils.EntryNotFoundError:
17
+ return False, "Please add a model card to your model to explain how you trained/fine-tuned it."
18
+
19
+ # Enforce license metadata
20
+ if card.data.license is None:
21
+ if not ("license_name" in card.data and "license_link" in card.data):
22
+ return False, (
23
+ "License not found. Please add a license to your model card using the `license` metadata or a"
24
+ " `license_name`/`license_link` pair."
25
+ )
26
+
27
+ # Enforce card content
28
+ if len(card.text) < 200:
29
+ return False, "Please add a description to your model card, it is too short."
30
+
31
+ return True, ""
32
+
33
+
34
+ def is_model_on_hub(
35
+ model_name: str, revision: str, token: str = None, trust_remote_code=False, test_tokenizer=False
36
+ ) -> tuple[bool, str]:
37
+ """Checks if the model model_name is on the hub, and whether it (and its tokenizer) can be loaded with AutoClasses."""
38
+ try:
39
+ config = AutoConfig.from_pretrained(
40
+ model_name, revision=revision, trust_remote_code=trust_remote_code, token=token
41
+ )
42
+ if test_tokenizer:
43
+ try:
44
+ AutoTokenizer.from_pretrained(
45
+ model_name, revision=revision, trust_remote_code=trust_remote_code, token=token
46
+ )
47
+ except ValueError as e:
48
+ return (False, f"uses a tokenizer which is not in a transformers release: {e}", None)
49
+ except Exception:
50
+ return (
51
+ False,
52
+ "'s tokenizer cannot be loaded. Is your tokenizer class in a stable transformers release, and correctly configured?",
53
+ None,
54
+ )
55
+ return True, None, config
56
+
57
+ except ValueError:
58
+ return (
59
+ False,
60
+ "needs to be launched with `trust_remote_code=True`. For safety reason, we do not allow these models to be automatically submitted to the leaderboard.",
61
+ None,
62
+ )
63
+
64
+ except Exception:
65
+ return False, "was not found on hub!", None
66
+
67
+
68
+ def get_model_size(model_info: ModelInfo, precision: str):
69
+ """Gets the model size from the configuration, or the model name if the configuration does not contain the information."""
70
+ try:
71
+ model_size = round(model_info.safetensors["total"] / 1e9, 3)
72
+ except (AttributeError, TypeError):
73
+ return 0 # Unknown model sizes are indicated as 0, see NUMERIC_INTERVALS in app.py
74
+
75
+ size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.modelId.lower()) else 1
76
+ model_size = size_factor * model_size
77
+ return model_size
78
+
79
+
80
+ def get_model_arch(model_info: ModelInfo):
81
+ """Gets the model architecture from the configuration"""
82
+ return model_info.config.get("architectures", "Unknown")
83
+
84
+
85
+ def already_submitted_models(requested_models_dir: str) -> set[str]:
86
+ """Gather a list of already submitted models to avoid duplicates"""
87
+ depth = 1
88
+ file_names = []
89
+ users_to_submission_dates = defaultdict(list)
90
+
91
+ for root, _, files in os.walk(requested_models_dir):
92
+ current_depth = root.count(os.sep) - requested_models_dir.count(os.sep)
93
+ if current_depth == depth:
94
+ for file in files:
95
+ if not file.endswith(".json"):
96
+ continue
97
+ with open(os.path.join(root, file), "r") as f:
98
+ info = json.load(f)
99
+ file_names.append(f"{info['model']}_{info['revision']}_{info['precision']}")
100
+
101
+ # Select organisation
102
+ if info["model"].count("/") == 0 or "submitted_time" not in info:
103
+ continue
104
+ organisation, _ = info["model"].split("/")
105
+ users_to_submission_dates[organisation].append(info["submitted_time"])
106
+
107
+ return set(file_names), users_to_submission_dates
src/submission/submit.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from datetime import datetime, timezone
5
+
6
+ from src.envs import API, EVAL_REQUESTS_PATH, QUEUE_REPO, TOKEN
7
+ from src.submission.check_validity import (
8
+ already_submitted_models,
9
+ check_model_card,
10
+ get_model_size,
11
+ is_model_on_hub,
12
+ )
13
+
14
+ REQUESTED_MODELS = None
15
+ USERS_TO_SUBMISSION_DATES = None
16
+ MODEL_ID_PATTERN = re.compile(r"^[\w.-]+/[\w.-]+$")
17
+
18
+
19
+ def response(status: str, message: str) -> dict[str, str]:
20
+ return {"status": status, "message": message}
21
+
22
+
23
+ def add_new_eval(
24
+ model: str,
25
+ base_model: str,
26
+ revision: str,
27
+ precision: str,
28
+ weight_type: str,
29
+ model_type: str,
30
+ ):
31
+ global REQUESTED_MODELS
32
+ global USERS_TO_SUBMISSION_DATES
33
+ if REQUESTED_MODELS is None:
34
+ REQUESTED_MODELS, USERS_TO_SUBMISSION_DATES = already_submitted_models(EVAL_REQUESTS_PATH)
35
+
36
+ model = model.strip()
37
+ base_model = base_model.strip()
38
+ revision = revision.strip() or "main"
39
+ if not MODEL_ID_PATTERN.fullmatch(model):
40
+ return response("error", "Enter a model ID in the form organization/model-name.")
41
+
42
+ user_name, model_path = model.split("/", 1)
43
+ precision = precision.split(" ")[0]
44
+ current_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
45
+
46
+ if not model_type:
47
+ return response("error", "Please select a model type.")
48
+
49
+ if weight_type in ["Delta", "Adapter"]:
50
+ if not MODEL_ID_PATTERN.fullmatch(base_model):
51
+ return response("error", "Enter a base model ID in the form organization/model-name.")
52
+ base_model_on_hub, error, _ = is_model_on_hub(
53
+ model_name=base_model,
54
+ revision=revision,
55
+ token=TOKEN,
56
+ test_tokenizer=True,
57
+ )
58
+ if not base_model_on_hub:
59
+ return response("error", f'Base model "{base_model}" {error}')
60
+
61
+ if weight_type != "Adapter":
62
+ model_on_hub, error, _ = is_model_on_hub(
63
+ model_name=model,
64
+ revision=revision,
65
+ token=TOKEN,
66
+ test_tokenizer=True,
67
+ )
68
+ if not model_on_hub:
69
+ return response("error", f'Model "{model}" {error}')
70
+
71
+ try:
72
+ model_info = API.model_info(repo_id=model, revision=revision)
73
+ except Exception:
74
+ return response("error", "Could not read this model. Check its ID and revision, then try again.")
75
+
76
+ model_size = get_model_size(model_info=model_info, precision=precision)
77
+ try:
78
+ license_name = model_info.cardData["license"]
79
+ except Exception:
80
+ return response("error", "Please select a license in your model card.")
81
+
82
+ modelcard_ok, error_msg = check_model_card(model)
83
+ if not modelcard_ok:
84
+ return response("error", error_msg)
85
+
86
+ eval_entry = {
87
+ "model": model,
88
+ "base_model": base_model,
89
+ "revision": revision,
90
+ "precision": precision,
91
+ "weight_type": weight_type,
92
+ "status": "PENDING",
93
+ "submitted_time": current_time,
94
+ "model_type": model_type,
95
+ "likes": model_info.likes,
96
+ "params": model_size,
97
+ "license": license_name,
98
+ "private": False,
99
+ }
100
+
101
+ request_key = f"{model}_{revision}_{precision}"
102
+ if request_key in REQUESTED_MODELS:
103
+ return response("warning", "This model has already been submitted.")
104
+
105
+ out_dir = os.path.join(EVAL_REQUESTS_PATH, user_name)
106
+ os.makedirs(out_dir, exist_ok=True)
107
+ out_path = os.path.join(out_dir, f"{model_path}_eval_request_False_{precision}_{weight_type}.json")
108
+ with open(out_path, "w") as file:
109
+ json.dump(eval_entry, file)
110
+
111
+ try:
112
+ API.upload_file(
113
+ path_or_fileobj=out_path,
114
+ path_in_repo=os.path.relpath(out_path, EVAL_REQUESTS_PATH),
115
+ repo_id=QUEUE_REPO,
116
+ repo_type="dataset",
117
+ commit_message=f"Add {model} to eval queue",
118
+ )
119
+ except Exception:
120
+ os.remove(out_path)
121
+ return response("error", "The model passed validation, but the queue could not be updated. Please try again.")
122
+
123
+ REQUESTED_MODELS.add(request_key)
124
+ return response(
125
+ "success",
126
+ "Your model is in the evaluation queue. It should appear in the pending list shortly.",
127
+ )
static/app.js ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js";
2
+
3
+ const state = {
4
+ api: null,
5
+ payload: null,
6
+ rows: [],
7
+ sort: { key: "Average ⬆️", direction: "desc" },
8
+ };
9
+
10
+ let columns = [];
11
+
12
+ function configureColumns(benchmarks = []) {
13
+ columns = [
14
+ { key: "rank", label: "Rank" },
15
+ { key: "Model", label: "Model" },
16
+ { key: "Average ⬆️", label: "Average", score: true },
17
+ ...benchmarks.map((benchmark) => ({ key: benchmark, label: benchmark, score: true })),
18
+ { key: "Type", label: "Type" },
19
+ { key: "Precision", label: "Precision" },
20
+ { key: "#Params (B)", label: "Params (B)", score: true },
21
+ { key: "Hub License", label: "License" },
22
+ { key: "Hub ❤️", label: "Likes", score: true },
23
+ { key: "Available on the hub", label: "Hub" },
24
+ ];
25
+ }
26
+
27
+ const $ = (selector) => document.querySelector(selector);
28
+ const $$ = (selector) => [...document.querySelectorAll(selector)];
29
+ const escapeHtml = (value) => String(value ?? "").replace(/[&<>'"]/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", "'": "&#39;", '"': "&quot;" })[char]);
30
+ const safeModelId = (value) => /^[\w.-]+\/[\w.-]+$/.test(String(value || "")) ? value : "";
31
+ const formatScore = (value) => value == null || Number.isNaN(Number(value)) ? "—" : Number(value).toFixed(2);
32
+ const firstData = (response) => Array.isArray(response.data) ? response.data[0] : response.data;
33
+
34
+ function markdown(source = "") {
35
+ let text = escapeHtml(source.trim());
36
+ text = text.replace(/```(?:[a-zA-Z0-9_+-]+)?\n([\s\S]*?)```/g, (_, code) => `<pre><code>${code.trim()}</code></pre>`);
37
+ text = text.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>");
38
+ text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>');
39
+ text = text.replace(/`([^`]+)`/g, "<code>$1</code>");
40
+ return text.split(/\n{2,}/).map((block) => /^<(h2|h3|pre)/.test(block) ? block : `<p>${block.replace(/\n/g, "<br>")}</p>`).join("");
41
+ }
42
+
43
+ function renderHead() {
44
+ $("#leaderboard-head").innerHTML = `<tr>${columns.map((column) => {
45
+ const sortable = column.score || column.key === "Model";
46
+ const active = state.sort.key === column.key;
47
+ const arrow = active ? (state.sort.direction === "asc" ? " ↑" : " ↓") : "";
48
+ return `<th class="${sortable ? "sortable" : ""} ${active ? "active-sort" : ""}" data-key="${escapeHtml(column.key)}">${column.label}${arrow}</th>`;
49
+ }).join("")}</tr>`;
50
+ $$("th.sortable").forEach((header) => header.addEventListener("click", () => {
51
+ const key = header.dataset.key;
52
+ state.sort = { key, direction: state.sort.key === key && state.sort.direction === "desc" ? "asc" : "desc" };
53
+ renderHead();
54
+ renderRows();
55
+ }));
56
+ }
57
+
58
+ function filteredRows() {
59
+ const query = $("#search").value.trim().toLowerCase();
60
+ const type = $("#type-filter").value;
61
+ const precision = $("#precision-filter").value;
62
+ const available = $("#available-filter").checked;
63
+ const rows = state.rows.filter((row) => {
64
+ const haystack = `${row.Model || ""} ${row["Hub License"] || ""}`.toLowerCase();
65
+ return (!query || haystack.includes(query)) && (!type || row.Type === type) && (!precision || row.Precision === precision) && (!available || row["Available on the hub"] === true);
66
+ });
67
+ const multiplier = state.sort.direction === "asc" ? 1 : -1;
68
+ return rows.sort((a, b) => {
69
+ const av = a[state.sort.key];
70
+ const bv = b[state.sort.key];
71
+ if (typeof av === "number" && typeof bv === "number") return (av - bv) * multiplier;
72
+ return String(av ?? "").localeCompare(String(bv ?? "")) * multiplier;
73
+ });
74
+ }
75
+
76
+ function cell(row, column, rank) {
77
+ const value = row[column.key];
78
+ if (column.key === "rank") return `<td class="rank ${rank < 3 ? "top" : ""}">${String(rank + 1).padStart(2, "0")}</td>`;
79
+ if (column.key === "Model") {
80
+ const id = safeModelId(value);
81
+ return `<td class="model-cell">${id ? `<a href="https://huggingface.co/${encodeURI(id)}" target="_blank" rel="noreferrer">${escapeHtml(id)}</a>` : escapeHtml(value)}</td>`;
82
+ }
83
+ if (column.key === "Available on the hub") return `<td><span class="availability ${value === true ? "yes" : ""}" title="${value === true ? "Available" : "Unavailable"}"></span></td>`;
84
+ if (["Type", "Precision", "Hub License"].includes(column.key)) return `<td><span class="pill">${escapeHtml(value || "—")}</span></td>`;
85
+ if (column.score) return `<td class="score ${column.key === "Average ⬆️" ? "primary" : ""}">${formatScore(value)}</td>`;
86
+ return `<td>${escapeHtml(value ?? "—")}</td>`;
87
+ }
88
+
89
+ function renderRows() {
90
+ const rows = filteredRows();
91
+ $("#leaderboard-body").innerHTML = rows.map((row, rank) => `<tr>${columns.map((column) => cell(row, column, rank)).join("")}</tr>`).join("");
92
+ $(".table-shell").classList.toggle("is-empty", rows.length === 0);
93
+ $("#empty-state").classList.toggle("hidden", rows.length > 0);
94
+ $("#result-count").textContent = `${rows.length} model${rows.length === 1 ? "" : "s"} shown`;
95
+ }
96
+
97
+ function populateSelect(selector, values, selected = "") {
98
+ const select = $(selector);
99
+ const existing = select.querySelector("option")?.outerHTML || "";
100
+ select.innerHTML = existing + values.map((value) => `<option value="${escapeHtml(value)}" ${value === selected ? "selected" : ""}>${escapeHtml(value)}</option>`).join("");
101
+ }
102
+
103
+ function renderQueues(queues) {
104
+ const definitions = [
105
+ ["finished", "Finished", "Recently completed"],
106
+ ["running", "Running", "Evaluations in progress"],
107
+ ["pending", "Pending", "Waiting to be evaluated"],
108
+ ];
109
+ $("#queue-grid").innerHTML = definitions.map(([key, label, help]) => {
110
+ const rows = queues[key] || [];
111
+ const latest = rows[0]?.model ? `Latest: ${escapeHtml(rows[0].model)}` : help;
112
+ return `<article class="queue-card ${key}"><header><h3>${label}</h3><strong>${rows.length}</strong></header><p>${latest}</p></article>`;
113
+ }).join("");
114
+ }
115
+
116
+ function applyPayload(payload) {
117
+ state.payload = payload;
118
+ state.rows = payload.rows || [];
119
+ configureColumns(payload.benchmarks);
120
+ document.title = payload.copy?.title || "Open Leaderboard";
121
+ $("#leaderboard-name").textContent = payload.copy?.title || "Open Leaderboard";
122
+ $("#updated-at").textContent = `Updated ${new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(payload.updated_at))}`;
123
+ $("#data-warning").textContent = payload.error || "";
124
+ $("#data-warning").classList.toggle("hidden", !payload.error);
125
+
126
+ const types = [...new Set(state.rows.map((row) => row.Type).filter(Boolean))];
127
+ const precisions = [...new Set(state.rows.map((row) => row.Precision).filter(Boolean))];
128
+ populateSelect("#type-filter", types);
129
+ populateSelect("#precision-filter", precisions);
130
+ populateSelect('[name="model_type"]', payload.model_types || []);
131
+ populateSelect('[name="precision"]', payload.precisions || [], "float16");
132
+ populateSelect('[name="weight_type"]', payload.weight_types || [], "Original");
133
+
134
+ renderHead();
135
+ renderRows();
136
+ renderQueues(payload.queues || {});
137
+ $("#submission-copy").innerHTML = markdown(payload.copy?.submission);
138
+ $("#about-copy").innerHTML = markdown(payload.copy?.about);
139
+ if (payload.copy?.citation) {
140
+ $("#citation-card").classList.remove("hidden");
141
+ $("#citation-label").textContent = payload.copy.citation_label;
142
+ $("#citation").textContent = payload.copy.citation;
143
+ }
144
+ }
145
+
146
+ async function loadData() {
147
+ try {
148
+ state.api = await client(location.origin);
149
+ const response = await state.api.predict("/leaderboard_data", {});
150
+ applyPayload(firstData(response));
151
+ } catch (error) {
152
+ $("#leaderboard-body").innerHTML = "";
153
+ $(".table-shell").classList.add("is-empty");
154
+ $("#data-warning").textContent = `The leaderboard could not be loaded: ${error.message}`;
155
+ $("#data-warning").classList.remove("hidden");
156
+ $("#empty-state").classList.remove("hidden");
157
+ }
158
+ }
159
+
160
+ $$('.tab').forEach((tab) => tab.addEventListener("click", () => {
161
+ $$('.tab').forEach((item) => item.classList.toggle("active", item === tab));
162
+ $$('.panel').forEach((panel) => panel.classList.toggle("active", panel.id === `panel-${tab.dataset.tab}`));
163
+ history.replaceState(null, "", `#${tab.dataset.tab}`);
164
+ }));
165
+
166
+ [$("#search"), $("#type-filter"), $("#precision-filter"), $("#available-filter")].forEach((control) => control.addEventListener("input", renderRows));
167
+
168
+ $("#submit-form").addEventListener("submit", async (event) => {
169
+ event.preventDefault();
170
+ const button = event.currentTarget.querySelector("button[type=submit]");
171
+ const result = $("#submission-result");
172
+ const values = Object.fromEntries(new FormData(event.currentTarget));
173
+ button.disabled = true;
174
+ button.firstElementChild.textContent = "Validating model…";
175
+ result.className = "notice hidden";
176
+ try {
177
+ const response = await state.api.predict("/submit_model", values);
178
+ const data = firstData(response);
179
+ result.textContent = data.message;
180
+ result.className = `notice ${data.status}`;
181
+ if (data.status === "success") event.currentTarget.reset();
182
+ } catch (error) {
183
+ result.textContent = `Submission failed: ${error.message}`;
184
+ result.className = "notice error";
185
+ } finally {
186
+ button.disabled = false;
187
+ button.firstElementChild.textContent = "Submit evaluation";
188
+ }
189
+ });
190
+
191
+ $("#copy-citation").addEventListener("click", async () => {
192
+ await navigator.clipboard.writeText(state.payload.copy.citation);
193
+ $("#copy-citation").textContent = "Copied";
194
+ setTimeout(() => $("#copy-citation").textContent = "Copy citation", 1600);
195
+ });
196
+
197
+ const initialTab = location.hash.slice(1);
198
+ if (["submit", "about"].includes(initialTab)) $(`.tab[data-tab="${initialTab}"]`).click();
199
+ loadData();
static/index.html ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="description" content="A customizable leaderboard for comparing model evaluation results." />
7
+ <title>Open Leaderboard</title>
8
+ <link rel="stylesheet" href="/static/styles.css" />
9
+ </head>
10
+ <body>
11
+ <main>
12
+ <header class="app-header">
13
+ <h1 id="leaderboard-name">Open Leaderboard</h1>
14
+ </header>
15
+
16
+ <nav class="tabs" aria-label="Leaderboard sections">
17
+ <button class="tab active" data-tab="leaderboard">
18
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2" /></svg>
19
+ <span>Leaderboard</span>
20
+ </button>
21
+ <button class="tab" data-tab="submit">
22
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 16V3M7 8l5-5 5 5M5 21h14" /></svg>
23
+ <span>Submit</span>
24
+ </button>
25
+ <button class="tab" data-tab="about">
26
+ <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9" /><path d="M12 11v5M12 8h.01" /></svg>
27
+ <span>About</span>
28
+ </button>
29
+ </nav>
30
+
31
+ <section class="panel active" id="panel-leaderboard">
32
+ <div class="panel-heading">
33
+ <div><h2>LLM Benchmark</h2><p>Compare evaluation results across models.</p></div>
34
+ <p id="updated-at" class="updated">Loading the latest results…</p>
35
+ </div>
36
+
37
+ <div class="toolbar">
38
+ <label class="search"><span>⌕</span><input id="search" type="search" placeholder="Search models or licenses" /></label>
39
+ <label class="select-control"><span>Model type</span><select id="type-filter"><option value="">All types</option></select></label>
40
+ <label class="select-control"><span>Precision</span><select id="precision-filter"><option value="">Any precision</option></select></label>
41
+ <label class="toggle"><input id="available-filter" type="checkbox" /> Available only</label>
42
+ </div>
43
+
44
+ <div id="data-warning" class="notice warning hidden"></div>
45
+ <div class="table-shell">
46
+ <table>
47
+ <thead id="leaderboard-head"></thead>
48
+ <tbody id="leaderboard-body">
49
+ <tr class="loading-row"><td><span class="skeleton"></span></td><td><span class="skeleton wide"></span></td><td><span class="skeleton"></span></td><td><span class="skeleton"></span></td></tr>
50
+ </tbody>
51
+ </table>
52
+ <div class="empty-state hidden" id="empty-state"><h3>No models found</h3><p>Try clearing one of your filters.</p></div>
53
+ </div>
54
+ <div class="table-footer"><span id="result-count">—</span><span>Click a score heading to sort</span></div>
55
+ </section>
56
+
57
+ <section class="panel" id="panel-submit">
58
+ <div class="panel-heading">
59
+ <div><h2>Submit a model</h2><p>Validate a public Hugging Face model and add it to the evaluation queue.</p></div>
60
+ </div>
61
+
62
+ <div class="queue-grid" id="queue-grid"></div>
63
+
64
+ <div class="submit-layout">
65
+ <form id="submit-form" class="block submit-card">
66
+ <div class="block-heading"><h3>Model details</h3><p>Required fields must be completed before submission.</p></div>
67
+ <div class="form-grid">
68
+ <label class="field full"><span>Model ID <b>required</b></span><input name="model" placeholder="organization/model-name" required /></label>
69
+ <label class="field"><span>Revision</span><input name="revision" placeholder="main" /></label>
70
+ <label class="field"><span>Model type <b>required</b></span><select name="model_type" required><option value="">Choose a type</option></select></label>
71
+ <label class="field"><span>Precision <b>required</b></span><select name="precision" required></select></label>
72
+ <label class="field"><span>Weights <b>required</b></span><select name="weight_type" required></select></label>
73
+ <label class="field full"><span>Base model <small>for adapter or delta weights</small></span><input name="base_model" placeholder="organization/base-model" /></label>
74
+ </div>
75
+ <button class="primary-button" type="submit"><span>Submit evaluation</span></button>
76
+ <div id="submission-result" class="notice hidden" role="status"></div>
77
+ </form>
78
+
79
+ <aside class="block guidance-card"><h3>Before you submit</h3><div id="submission-copy" class="prose"></div></aside>
80
+ </div>
81
+ </section>
82
+
83
+ <section class="panel" id="panel-about">
84
+ <div class="panel-heading"><div><h2>About</h2><p>Methodology and reproducibility information.</p></div></div>
85
+ <div id="about-copy" class="block prose about-copy"></div>
86
+ <div id="citation-card" class="block citation-card hidden"><div><h3 id="citation-label"></h3></div><button id="copy-citation">Copy</button><pre id="citation"></pre></div>
87
+ </section>
88
+ </main>
89
+
90
+ <script type="module" src="/static/app.js"></script>
91
+ </body>
92
+ </html>
static/styles.css ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: light;
3
+ --background: #ffffff;
4
+ --block: #ffffff;
5
+ --text: #111827;
6
+ --body-text: #374151;
7
+ --muted: #6b7280;
8
+ --border: #e5e7eb;
9
+ --border-strong: #d1d5db;
10
+ --primary: #f97316;
11
+ --primary-hover: #ea580c;
12
+ --primary-soft: #fff7ed;
13
+ --success: #15803d;
14
+ --warning: #a16207;
15
+ --danger: #b91c1c;
16
+ }
17
+
18
+ * { box-sizing: border-box; }
19
+ body { margin: 0; min-height: 100vh; background: var(--background); color: var(--text); font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 14px; }
20
+ button, input, select { font: inherit; }
21
+ button { cursor: pointer; }
22
+ main { width: min(1180px, calc(100% - 32px)); margin: 0 auto; padding: 36px 0 52px; }
23
+
24
+ .app-header { max-width: 760px; margin: 0 auto 28px; text-align: center; }
25
+ .app-header h1 { margin: 0 0 10px; font-size: 32px; line-height: 1.2; font-weight: 700; }
26
+ .tabs { display: flex; justify-content: center; gap: 4px; margin-bottom: 24px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
27
+ .tab { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 8px 14px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--muted); font-weight: 500; }
28
+ .tab svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
29
+ .tab:hover { color: var(--text); background: #f3f4f6; }
30
+ .tab.active { color: var(--text); background: var(--block); border-color: var(--border); box-shadow: 0 1px 2px rgba(0, 0, 0, .04); }
31
+
32
+ .panel { display: none; }
33
+ .panel.active { display: block; }
34
+ .panel-heading { display: flex; align-items: end; justify-content: space-between; gap: 24px; margin-bottom: 14px; }
35
+ .panel-heading h2 { margin: 0 0 4px; font-size: 20px; line-height: 1.3; }
36
+ .panel-heading p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.45; }
37
+ .updated { white-space: nowrap; font-size: 11px !important; }
38
+
39
+ .block, .toolbar, .table-shell, .queue-card { background: var(--block); border: 1px solid var(--border); border-radius: 10px; }
40
+ .toolbar { display: grid; grid-template-columns: minmax(240px, 1fr) auto auto auto; gap: 10px; align-items: end; padding: 12px; margin-bottom: 12px; }
41
+ .search { display: flex; align-items: center; gap: 8px; height: 38px; padding: 0 11px; border: 1px solid var(--border-strong); border-radius: 7px; background: var(--block); }
42
+ .search > span { color: var(--muted); font-size: 18px; }
43
+ .search input { width: 100%; border: 0; outline: 0; background: transparent; color: var(--text); font-size: 13px; }
44
+ .select-control { display: grid; gap: 4px; color: var(--muted); font-size: 11px; }
45
+ .select-control select { height: 38px; min-width: 130px; padding: 0 28px 0 10px; border: 1px solid var(--border-strong); border-radius: 7px; outline: 0; background: var(--block); color: var(--text); font-size: 13px; }
46
+ .toggle { display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 9px; color: var(--body-text); font-size: 12px; white-space: nowrap; }
47
+ .toggle input { width: 15px; height: 15px; accent-color: var(--primary); }
48
+
49
+ .table-shell { position: relative; overflow-x: auto; }
50
+ .table-shell.is-empty { min-height: 180px; }
51
+ table { width: 100%; border-collapse: collapse; font-size: 12px; }
52
+ th { height: 42px; padding: 0 12px; border-bottom: 1px solid var(--border); background: #f9fafb; color: var(--muted); text-align: right; font-size: 11px; font-weight: 600; white-space: nowrap; }
53
+ th:first-child, th:nth-child(2), td:first-child, td:nth-child(2) { text-align: left; }
54
+ th.sortable { cursor: pointer; user-select: none; }
55
+ th.sortable:hover, th.active-sort { color: var(--text); }
56
+ td { height: 54px; padding: 0 12px; border-bottom: 1px solid var(--border); text-align: right; white-space: nowrap; }
57
+ tbody tr:last-child td { border-bottom: 0; }
58
+ tbody tr:hover { background: #fafafa; }
59
+ .rank { width: 42px; color: var(--muted); }
60
+ .rank.top, .score.primary { color: var(--primary-hover); font-weight: 600; }
61
+ .model-cell a { display: inline-flex; align-items: center; gap: 5px; max-width: 290px; overflow: hidden; color: var(--text); font-weight: 600; text-decoration: none; text-overflow: ellipsis; }
62
+ .model-cell a::after { content: "↗"; color: var(--muted); font-size: 10px; }
63
+ .model-cell a:hover { color: var(--primary-hover); }
64
+ .pill { display: inline-flex; padding: 3px 7px; border-radius: 6px; background: #f3f4f6; color: var(--body-text); font-size: 11px; }
65
+ .availability { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: var(--danger); }
66
+ .availability.yes { background: var(--success); }
67
+ .table-footer { display: flex; justify-content: space-between; padding: 8px 3px 0; color: var(--muted); font-size: 11px; }
68
+ .empty-state { position: absolute; inset: 43px 0 0; display: grid; place-content: center; text-align: center; color: var(--muted); }
69
+ .empty-state h3 { margin: 0 0 4px; color: var(--body-text); font-size: 14px; }
70
+ .empty-state p { margin: 0; font-size: 12px; }
71
+ .skeleton { display: block; width: 48px; height: 10px; border-radius: 4px; background: #e5e7eb; }
72
+ .skeleton.wide { width: 160px; }
73
+
74
+ .queue-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 14px; }
75
+ .queue-card { padding: 14px; }
76
+ .queue-card header { display: flex; align-items: center; justify-content: space-between; }
77
+ .queue-card h3 { margin: 0; font-size: 13px; }
78
+ .queue-card strong { font-size: 18px; }
79
+ .queue-card p { margin: 7px 0 0; overflow: hidden; color: var(--muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
80
+ .queue-card.finished strong { color: var(--success); }
81
+ .queue-card.running strong { color: var(--primary-hover); }
82
+ .queue-card.pending strong { color: var(--warning); }
83
+
84
+ .submit-layout { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(300px, .85fr); gap: 14px; align-items: start; }
85
+ .submit-card, .guidance-card, .about-copy, .citation-card { padding: 18px; }
86
+ .block-heading { padding-bottom: 14px; border-bottom: 1px solid var(--border); }
87
+ .block-heading h3, .guidance-card > h3 { margin: 0 0 4px; font-size: 15px; }
88
+ .block-heading p { margin: 0; color: var(--muted); font-size: 12px; }
89
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px 12px; padding: 16px 0; }
90
+ .field { display: grid; gap: 6px; }
91
+ .field.full { grid-column: 1 / -1; }
92
+ .field > span { color: var(--body-text); font-size: 12px; font-weight: 500; }
93
+ .field b { margin-left: 3px; color: var(--muted); font-size: 10px; font-weight: 400; }
94
+ .field small { color: var(--muted); font-weight: 400; }
95
+ .field input, .field select { width: 100%; height: 39px; padding: 0 10px; border: 1px solid var(--border-strong); border-radius: 7px; outline: 0; background: var(--block); color: var(--text); font-size: 13px; }
96
+ .field input:focus, .field select:focus, .search:focus-within { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-soft); }
97
+ .primary-button { min-height: 38px; padding: 0 14px; border: 1px solid var(--primary-hover); border-radius: 7px; background: var(--primary); color: #fff; font-weight: 600; }
98
+ .primary-button:hover { background: var(--primary-hover); }
99
+ .primary-button:disabled { opacity: .6; cursor: wait; }
100
+ .guidance-card { max-height: 560px; overflow: auto; }
101
+
102
+ .prose { color: var(--body-text); font-size: 13px; line-height: 1.6; }
103
+ .prose h2, .prose h3 { color: var(--text); }
104
+ .prose h2 { margin: 20px 0 6px; font-size: 17px; }
105
+ .prose h3 { margin: 16px 0 5px; font-size: 14px; }
106
+ .prose p { margin: 7px 0; }
107
+ .prose pre { margin: 10px 0; overflow-wrap: anywhere; white-space: pre-wrap; padding: 11px; border: 1px solid var(--border); border-radius: 7px; background: #f9fafb; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
108
+ .prose code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .92em; }
109
+ .prose pre code { font-size: inherit; }
110
+ .prose a { color: #2563eb; }
111
+ .about-copy { min-height: 160px; }
112
+ .about-copy h2:first-child { margin-top: 0; }
113
+ .citation-card { display: grid; grid-template-columns: 1fr auto; gap: 12px; margin-top: 12px; }
114
+ .citation-card h3 { margin: 5px 0 0; font-size: 14px; }
115
+ .citation-card button { align-self: start; padding: 6px 10px; border: 1px solid var(--border-strong); border-radius: 6px; background: var(--block); color: var(--text); }
116
+ .citation-card pre { grid-column: 1 / -1; overflow: auto; margin: 0; padding: 10px; border-radius: 6px; background: #f9fafb; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }
117
+
118
+ .notice { margin: 10px 0; padding: 10px 12px; border: 1px solid; border-radius: 7px; font-size: 12px; line-height: 1.45; }
119
+ .notice.warning { color: var(--warning); background: #fffbeb; border-color: #fde68a; }
120
+ .notice.error { color: var(--danger); background: #fef2f2; border-color: #fecaca; }
121
+ .notice.success { color: var(--success); background: #f0fdf4; border-color: #bbf7d0; }
122
+ .hidden { display: none !important; }
123
+ @media (max-width: 820px) {
124
+ .toolbar { grid-template-columns: 1fr 1fr; }
125
+ .search { grid-column: 1 / -1; }
126
+ .submit-layout { grid-template-columns: 1fr; }
127
+ .guidance-card { max-height: none; }
128
+ }
129
+
130
+ @media (max-width: 560px) {
131
+ main { width: min(100% - 24px, 1180px); padding-top: 24px; }
132
+ .app-header h1 { font-size: 26px; }
133
+ .tabs { justify-content: stretch; }
134
+ .tab { flex: 1; padding-inline: 8px; }
135
+ .panel-heading { display: block; }
136
+ .updated { margin-top: 6px !important; }
137
+ .toolbar { grid-template-columns: 1fr; }
138
+ .search { grid-column: auto; }
139
+ .select-control select { width: 100%; }
140
+ .queue-grid, .form-grid { grid-template-columns: 1fr; }
141
+ .field.full { grid-column: auto; }
142
+ .table-footer span:last-child { display: none; }
143
+ }