bruAristimunha commited on
Commit
6f5cc9b
·
1 Parent(s): 8d9d190

Add HF eval-results benchmark registry and API endpoint

Browse files

- Add canonical EEG_BENCHMARKS registry (8 datasets) in config/benchmarks.py
- Add GET /api/benchmarks endpoint for benchmark metadata
- Refactor leaderboard transform_data to loop over registry instead of hardcoding
- Extract _build_entry_id helper for consistent unique ID generation
- Add push_eval_yaml.py script to register benchmarks on HF Hub
- Add pyyaml dependency

backend/app/api/endpoints/benchmarks.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import asdict
2
+
3
+ from fastapi import APIRouter, HTTPException
4
+ from typing import Any, Dict, List
5
+
6
+ from app.config.benchmarks import EEG_BENCHMARKS
7
+
8
+ router = APIRouter()
9
+
10
+ # Fields that are internal implementation details, not exposed via API
11
+ _INTERNAL_FIELDS = {"accuracy_field"}
12
+
13
+
14
+ def _benchmark_to_dict(key: str, benchmark) -> Dict[str, Any]:
15
+ """Convert a BenchmarkInfo dataclass to a JSON-serializable dict."""
16
+ d = asdict(benchmark)
17
+ for field in _INTERNAL_FIELDS:
18
+ d.pop(field, None)
19
+ d["key"] = key
20
+ return d
21
+
22
+
23
+ @router.get("")
24
+ async def list_benchmarks() -> List[Dict[str, Any]]:
25
+ """Return all registered EEG benchmarks with metadata."""
26
+ return [_benchmark_to_dict(k, b) for k, b in EEG_BENCHMARKS.items()]
27
+
28
+
29
+ @router.get("/{key}")
30
+ async def get_benchmark(key: str) -> Dict[str, Any]:
31
+ """Return a single benchmark's details by key."""
32
+ benchmark = EEG_BENCHMARKS.get(key)
33
+ if not benchmark:
34
+ raise HTTPException(
35
+ status_code=404,
36
+ detail=f"Benchmark '{key}' not found. Available: {list(EEG_BENCHMARKS.keys())}",
37
+ )
38
+ return _benchmark_to_dict(key, benchmark)
backend/app/api/router.py CHANGED
@@ -1,9 +1,10 @@
1
  from fastapi import APIRouter
2
 
3
- from app.api.endpoints import leaderboard, votes, models
4
 
5
  router = APIRouter()
6
 
7
  router.include_router(leaderboard.router, prefix="/leaderboard", tags=["leaderboard"])
8
  router.include_router(votes.router, prefix="/votes", tags=["votes"])
9
- router.include_router(models.router, prefix="/models", tags=["models"])
 
 
1
  from fastapi import APIRouter
2
 
3
+ from app.api.endpoints import leaderboard, votes, models, benchmarks
4
 
5
  router = APIRouter()
6
 
7
  router.include_router(leaderboard.router, prefix="/leaderboard", tags=["leaderboard"])
8
  router.include_router(votes.router, prefix="/votes", tags=["votes"])
9
+ router.include_router(models.router, prefix="/models", tags=["models"])
10
+ router.include_router(benchmarks.router, prefix="/benchmarks", tags=["benchmarks"])
backend/app/config/benchmarks.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Canonical benchmark registry for the EEG Finetune Arena.
3
+
4
+ Maps internal benchmark keys to HuggingFace dataset IDs, task IDs,
5
+ and metadata used by both eval.yaml generation and eval results pushing.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Dict
10
+
11
+
12
+ @dataclass(frozen=True)
13
+ class BenchmarkInfo:
14
+ """Metadata for a single EEG benchmark dataset."""
15
+
16
+ dataset_id: str
17
+ task_id: str
18
+ display_name: str
19
+ description: str
20
+ category: str
21
+ num_classes: int
22
+ accuracy_field: str
23
+ split: str = "test"
24
+ config: str = "default"
25
+
26
+
27
+ EEG_BENCHMARKS: Dict[str, BenchmarkInfo] = {
28
+ "bcic2a": BenchmarkInfo(
29
+ dataset_id="braindecode/bcic-2a",
30
+ task_id="motor_imagery_4class",
31
+ display_name="BCIC-2a",
32
+ description="BCI Competition IV Dataset 2a - 4-class motor imagery classification.",
33
+ category="Motor Imagery",
34
+ num_classes=4,
35
+ accuracy_field="bcic2a_accuracy",
36
+ ),
37
+ "physionet": BenchmarkInfo(
38
+ dataset_id="braindecode/physionet-mi",
39
+ task_id="motor_imagery_4class",
40
+ display_name="PhysioNet MI",
41
+ description="PhysioNet Motor Imagery dataset - 4-class motor imagery classification.",
42
+ category="Motor Imagery",
43
+ num_classes=4,
44
+ accuracy_field="physionet_accuracy",
45
+ ),
46
+ "isruc_sleep": BenchmarkInfo(
47
+ dataset_id="braindecode/isruc-sleep",
48
+ task_id="sleep_staging_5class",
49
+ display_name="ISRUC-SLEEP",
50
+ description="ISRUC-SLEEP dataset - 5-class sleep staging classification.",
51
+ category="Sleep Staging",
52
+ num_classes=5,
53
+ accuracy_field="isruc_sleep_accuracy",
54
+ ),
55
+ "tuab": BenchmarkInfo(
56
+ dataset_id="braindecode/tuab",
57
+ task_id="pathology_binary",
58
+ display_name="TUAB",
59
+ description="Temple University Abnormal EEG corpus - binary pathology detection.",
60
+ category="Pathology Detection",
61
+ num_classes=2,
62
+ accuracy_field="tuab_accuracy",
63
+ ),
64
+ "tuev": BenchmarkInfo(
65
+ dataset_id="braindecode/tuev",
66
+ task_id="event_classification_6class",
67
+ display_name="TUEV",
68
+ description="Temple University EEG Events corpus - 6-class event classification.",
69
+ category="Pathology Detection",
70
+ num_classes=6,
71
+ accuracy_field="tuev_accuracy",
72
+ ),
73
+ "chbmit": BenchmarkInfo(
74
+ dataset_id="braindecode/chb-mit",
75
+ task_id="seizure_detection_binary",
76
+ display_name="CHB-MIT",
77
+ description="CHB-MIT Scalp EEG dataset - binary seizure detection.",
78
+ category="Seizure Detection",
79
+ num_classes=2,
80
+ accuracy_field="chbmit_accuracy",
81
+ ),
82
+ "faced": BenchmarkInfo(
83
+ dataset_id="braindecode/faced",
84
+ task_id="emotion_recognition_9class",
85
+ display_name="FACED",
86
+ description="FACED dataset - 9-class emotion recognition from EEG.",
87
+ category="Emotion Recognition",
88
+ num_classes=9,
89
+ accuracy_field="faced_accuracy",
90
+ ),
91
+ "seedv": BenchmarkInfo(
92
+ dataset_id="braindecode/seed-v",
93
+ task_id="emotion_recognition_5class",
94
+ display_name="SEED-V",
95
+ description="SEED-V dataset - 5-class emotion recognition from EEG.",
96
+ category="Emotion Recognition",
97
+ num_classes=5,
98
+ accuracy_field="seedv_accuracy",
99
+ ),
100
+ }
101
+
102
+ EVALUATION_FRAMEWORK = "eeg-finetune-arena"
backend/app/services/leaderboard.py CHANGED
@@ -1,5 +1,4 @@
1
  from app.core.cache import cache_config
2
- from datetime import datetime
3
  from typing import List, Dict, Any
4
  from pathlib import Path
5
  import json
@@ -7,6 +6,7 @@ import datasets
7
  from fastapi import HTTPException
8
  import logging
9
  from app.config.base import HF_ORGANIZATION
 
10
  from app.core.formatting import LogFormatter
11
 
12
  logger = logging.getLogger(__name__)
@@ -15,6 +15,16 @@ logger = logging.getLogger(__name__)
15
  SAMPLE_DATA_PATH = Path(__file__).parent.parent / "data" / "sample_results.json"
16
 
17
 
 
 
 
 
 
 
 
 
 
 
18
  class LeaderboardService:
19
  def __init__(self):
20
  pass
@@ -145,58 +155,17 @@ class LeaderboardService:
145
  model_name = data.get("fullname", "Unknown")
146
  logger.debug(LogFormatter.info(f"Transforming data for model: {model_name}"))
147
 
148
- # Create unique ID combining model name, adapter method and dataset
149
- unique_id = (
150
- f"{data.get('fullname', 'Unknown')}"
151
- f"_{data.get('adapter', 'Unknown')}"
152
- f"_{data.get('Precision', 'Unknown')}"
153
- f"_{data.get('Model sha', 'Unknown')}"
154
- )
155
-
156
- # EEG benchmark evaluations — each dataset is a separate evaluation
157
- # Scores are accuracy (0-1 raw) and normalized to 0-100 scale
158
- evaluations = {
159
- "bcic2a": {
160
- "name": "BCIC-2a",
161
- "value": data.get("bcic2a_accuracy", 0),
162
- "normalized_score": data.get("bcic2a_accuracy", 0) * 100 if data.get("bcic2a_accuracy") else 0,
163
- },
164
- "physionet": {
165
- "name": "PhysioNet MI",
166
- "value": data.get("physionet_accuracy", 0),
167
- "normalized_score": data.get("physionet_accuracy", 0) * 100 if data.get("physionet_accuracy") else 0,
168
- },
169
- "isruc_sleep": {
170
- "name": "ISRUC-SLEEP",
171
- "value": data.get("isruc_sleep_accuracy", 0),
172
- "normalized_score": data.get("isruc_sleep_accuracy", 0) * 100 if data.get("isruc_sleep_accuracy") else 0,
173
- },
174
- "tuab": {
175
- "name": "TUAB",
176
- "value": data.get("tuab_accuracy", 0),
177
- "normalized_score": data.get("tuab_accuracy", 0) * 100 if data.get("tuab_accuracy") else 0,
178
- },
179
- "tuev": {
180
- "name": "TUEV",
181
- "value": data.get("tuev_accuracy", 0),
182
- "normalized_score": data.get("tuev_accuracy", 0) * 100 if data.get("tuev_accuracy") else 0,
183
- },
184
- "chbmit": {
185
- "name": "CHB-MIT",
186
- "value": data.get("chbmit_accuracy", 0),
187
- "normalized_score": data.get("chbmit_accuracy", 0) * 100 if data.get("chbmit_accuracy") else 0,
188
- },
189
- "faced": {
190
- "name": "FACED",
191
- "value": data.get("faced_accuracy", 0),
192
- "normalized_score": data.get("faced_accuracy", 0) * 100 if data.get("faced_accuracy") else 0,
193
- },
194
- "seedv": {
195
- "name": "SEED-V",
196
- "value": data.get("seedv_accuracy", 0),
197
- "normalized_score": data.get("seedv_accuracy", 0) * 100 if data.get("seedv_accuracy") else 0,
198
- },
199
- }
200
 
201
  features = {
202
  "is_not_available_on_hub": data.get("Available on the hub", False),
 
1
  from app.core.cache import cache_config
 
2
  from typing import List, Dict, Any
3
  from pathlib import Path
4
  import json
 
6
  from fastapi import HTTPException
7
  import logging
8
  from app.config.base import HF_ORGANIZATION
9
+ from app.config.benchmarks import EEG_BENCHMARKS
10
  from app.core.formatting import LogFormatter
11
 
12
  logger = logging.getLogger(__name__)
 
15
  SAMPLE_DATA_PATH = Path(__file__).parent.parent / "data" / "sample_results.json"
16
 
17
 
18
+ def _build_entry_id(data: Dict[str, Any]) -> str:
19
+ """Build a unique ID for a leaderboard entry."""
20
+ return (
21
+ f"{data.get('fullname', 'Unknown')}"
22
+ f"_{data.get('adapter', 'Unknown')}"
23
+ f"_{data.get('Precision', 'Unknown')}"
24
+ f"_{data.get('Model sha', 'Unknown')}"
25
+ )
26
+
27
+
28
  class LeaderboardService:
29
  def __init__(self):
30
  pass
 
155
  model_name = data.get("fullname", "Unknown")
156
  logger.debug(LogFormatter.info(f"Transforming data for model: {model_name}"))
157
 
158
+ unique_id = _build_entry_id(data)
159
+
160
+ # EEG benchmark evaluations from the canonical registry
161
+ evaluations = {}
162
+ for key, benchmark in EEG_BENCHMARKS.items():
163
+ score = data.get(benchmark.accuracy_field, 0)
164
+ evaluations[key] = {
165
+ "name": benchmark.display_name,
166
+ "value": score,
167
+ "normalized_score": score * 100 if score else 0,
168
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  features = {
171
  "is_not_available_on_hub": data.get("Available on the hub", False),
backend/pyproject.toml CHANGED
@@ -19,6 +19,7 @@ safetensors = "^0.5.3"
19
  aiofiles = "^24.1.0"
20
  fastapi-cache2 = "^0.2.1"
21
  python-dotenv = "^1.0.1"
 
22
 
23
  [tool.poetry.group.dev.dependencies]
24
  pytest = "^8.3.4"
 
19
  aiofiles = "^24.1.0"
20
  fastapi-cache2 = "^0.2.1"
21
  python-dotenv = "^1.0.1"
22
+ pyyaml = "^6.0"
23
 
24
  [tool.poetry.group.dev.dependencies]
25
  pytest = "^8.3.4"
backend/scripts/__init__.py ADDED
File without changes
backend/scripts/push_eval_yaml.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ One-time script to push eval.yaml files to benchmark dataset repos on HuggingFace.
3
+
4
+ This registers each EEG benchmark dataset with HF's decentralized eval system
5
+ by creating the dataset repo (if needed) and uploading an eval.yaml file.
6
+
7
+ Usage:
8
+ python -m scripts.push_eval_yaml
9
+ python -m scripts.push_eval_yaml --dry-run
10
+ python -m scripts.push_eval_yaml --benchmark bcic2a
11
+ """
12
+
13
+ import argparse
14
+ import sys
15
+ import logging
16
+
17
+ import yaml
18
+
19
+ from app.config.base import HF_TOKEN
20
+ from app.config.benchmarks import EEG_BENCHMARKS, EVALUATION_FRAMEWORK
21
+ from app.config.hf_config import API as hf_api
22
+ from app.core.formatting import LogFormatter
23
+
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def generate_eval_yaml(benchmark) -> str:
29
+ """Generate eval.yaml content for a benchmark dataset."""
30
+ eval_config = {
31
+ "name": f"{benchmark.display_name} {benchmark.category}",
32
+ "description": benchmark.description,
33
+ "evaluation_framework": EVALUATION_FRAMEWORK,
34
+ "tasks": [
35
+ {
36
+ "id": benchmark.task_id,
37
+ "config": benchmark.config,
38
+ "split": benchmark.split,
39
+ }
40
+ ],
41
+ }
42
+ return yaml.dump(eval_config, default_flow_style=False, sort_keys=False)
43
+
44
+
45
+ def push_eval_yaml(
46
+ benchmark_key: str = None,
47
+ dry_run: bool = False,
48
+ ):
49
+ """Push eval.yaml to benchmark dataset repos.
50
+
51
+ Args:
52
+ benchmark_key: If specified, only push for this benchmark.
53
+ dry_run: If True, print what would happen without pushing.
54
+ """
55
+ benchmarks = EEG_BENCHMARKS
56
+ if benchmark_key:
57
+ if benchmark_key not in benchmarks:
58
+ logger.error(f"Unknown benchmark key: {benchmark_key}")
59
+ logger.info(f"Available keys: {list(benchmarks.keys())}")
60
+ sys.exit(1)
61
+ benchmarks = {benchmark_key: benchmarks[benchmark_key]}
62
+
63
+ logger.info(LogFormatter.section("PUSHING EVAL.YAML TO BENCHMARK DATASETS"))
64
+
65
+ for key, benchmark in benchmarks.items():
66
+ dataset_id = benchmark.dataset_id
67
+ logger.info(LogFormatter.subsection(f"Processing: {dataset_id}"))
68
+
69
+ eval_yaml_content = generate_eval_yaml(benchmark)
70
+
71
+ if dry_run:
72
+ logger.info(f"[DRY RUN] Would create repo: {dataset_id} (type=dataset)")
73
+ logger.info(f"[DRY RUN] Would upload eval.yaml:\n{eval_yaml_content}")
74
+ continue
75
+
76
+ # Create the dataset repo if it doesn't exist
77
+ try:
78
+ hf_api.create_repo(
79
+ repo_id=dataset_id,
80
+ repo_type="dataset",
81
+ exist_ok=True,
82
+ )
83
+ logger.info(LogFormatter.success(f"Repo ready: {dataset_id}"))
84
+ except Exception as e:
85
+ logger.error(LogFormatter.error(f"Failed to create repo {dataset_id}", e))
86
+ continue
87
+
88
+ # Upload eval.yaml
89
+ try:
90
+ hf_api.upload_file(
91
+ path_or_fileobj=eval_yaml_content.encode("utf-8"),
92
+ path_in_repo="eval.yaml",
93
+ repo_id=dataset_id,
94
+ repo_type="dataset",
95
+ commit_message="Add eval.yaml for EEG Finetune Arena benchmark registration",
96
+ )
97
+ logger.info(LogFormatter.success(f"Uploaded eval.yaml to {dataset_id}"))
98
+ except Exception as e:
99
+ logger.error(
100
+ LogFormatter.error(f"Failed to upload eval.yaml to {dataset_id}", e)
101
+ )
102
+
103
+ logger.info(LogFormatter.section("DONE"))
104
+
105
+
106
+ def main():
107
+ parser = argparse.ArgumentParser(
108
+ description="Push eval.yaml files to HF benchmark dataset repos."
109
+ )
110
+ parser.add_argument(
111
+ "--dry-run",
112
+ action="store_true",
113
+ help="Preview without pushing to HF.",
114
+ )
115
+ parser.add_argument(
116
+ "--benchmark",
117
+ type=str,
118
+ default=None,
119
+ help="Only push for a specific benchmark key (e.g. bcic2a).",
120
+ )
121
+ args = parser.parse_args()
122
+
123
+ if not HF_TOKEN and not args.dry_run:
124
+ logger.error("HF_TOKEN environment variable is required (set it or use --dry-run)")
125
+ sys.exit(1)
126
+
127
+ push_eval_yaml(benchmark_key=args.benchmark, dry_run=args.dry_run)
128
+
129
+
130
+ if __name__ == "__main__":
131
+ main()